<?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: LinkBook</title>
    <description>The latest articles on DEV Community by LinkBook (@hamzezarenasiri).</description>
    <link>https://dev.to/hamzezarenasiri</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%2F386801%2Fb0d50e08-2c46-49f1-af4f-092440ecf4b9.png</url>
      <title>DEV Community: LinkBook</title>
      <link>https://dev.to/hamzezarenasiri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamzezarenasiri"/>
    <language>en</language>
    <item>
      <title>PostgreSQL 14 End of Life on 12 November 2026: How to Upgrade Postgres in Docker</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:27:42 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/postgresql-14-end-of-life-on-12-november-2026-how-to-upgrade-postgres-in-docker-1peo</link>
      <guid>https://dev.to/hamzezarenasiri/postgresql-14-end-of-life-on-12-november-2026-how-to-upgrade-postgres-in-docker-1peo</guid>
      <description>&lt;p&gt;Liquid syntax error: Unknown tag 'endraw'&lt;/p&gt;
</description>
      <category>postgres</category>
      <category>docker</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Roundcube 1.6.20 and 1.7.5: 20 Fixes, and an Older SQL Injection Now Being Exploited</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:46:54 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/roundcube-1620-and-175-20-fixes-and-an-older-sql-injection-now-being-exploited-22pk</link>
      <guid>https://dev.to/hamzezarenasiri/roundcube-1620-and-175-20-fixes-and-an-older-sql-injection-now-being-exploited-22pk</guid>
      <description>&lt;p&gt;On 19 October 2026 the Roundcube project released &lt;a href="https://roundcube.net/news/2026/10/19/security-updates-1.6.20-and-1.7.5" rel="noopener noreferrer"&gt;versions 1.6.20 and 1.7.5&lt;/a&gt;, with fixes for 20 reported vulnerabilities, and wrote: "We strongly recommend to update all productive installations of Roundcube 1.6.x and 1.7.x". Two weeks earlier, on 3 October, Singapore's Cyber Security Agency &lt;a href="https://www.csa.gov.sg/alerts-and-advisories/alerts/al-2026-131/" rel="noopener noreferrer"&gt;warned&lt;/a&gt; that an older Roundcube flaw, a SQL injection that works before login, "is reportedly being actively exploited".&lt;/p&gt;

&lt;p&gt;Those are two separate problems with different fixed versions. If your webmail runs Roundcube, here is how to tell which one applies to you and what to do this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exploited one: CVE-2026-48842
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-48842" rel="noopener noreferrer"&gt;NVD entry&lt;/a&gt;, Roundcube 1.6.x before 1.6.16 and 1.7.x before 1.7.1 has a pre-authentication SQL injection in the &lt;code&gt;virtuser_query&lt;/code&gt; plugin, caused by a backslash escape that slips past &lt;code&gt;preg_replace()&lt;/code&gt;. The fix shipped in May, in the &lt;a href="https://roundcube.net/news/2026/05/24/security-updates-1.6.16-and-1.7.1" rel="noopener noreferrer"&gt;1.6.16 and 1.7.1 releases of 24 May 2026&lt;/a&gt;. CSA rates it 8.1 under CVSS v3.1 and says it lets "an unauthenticated attacker" inject SQL into the underlying database.&lt;/p&gt;

&lt;p&gt;Two details decide whether you are exposed. First, the bug is in a plugin. &lt;code&gt;virtuser_query&lt;/code&gt; maps logins to mailbox users through a database query, and like every Roundcube plugin it only runs if it is listed in your configuration. Second, the fix is five months old, so a server that has taken any Roundcube update since late May is already past it. The servers at risk are the ones nobody has touched since spring, which is exactly the kind of box attackers scan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new batch: 1.6.20 and 1.7.5
&lt;/h2&gt;

&lt;p&gt;The October release fixes 20 issues. The project does not say any of them are being exploited. The NVD published several CVE records for them on 22 October, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114727" rel="noopener noreferrer"&gt;CVE-2026-114727&lt;/a&gt;, scored 8.3: potentially unsafe attachment content is passed to ImageMagick when images are resized.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114730" rel="noopener noreferrer"&gt;CVE-2026-114730&lt;/a&gt;, scored 7.2: a bypass of the server-side request forgery filter through the NAT64 range &lt;code&gt;64:ff9b::/96&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114732" rel="noopener noreferrer"&gt;CVE-2026-114732&lt;/a&gt;, scored 6.8: on multi-host installations, an alias login was not tied to its IMAP host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the list, from the release post, is worth reading if you run Roundcube with extra plugins or drivers: IMAP command injection through CRLF in the &lt;code&gt;_uid&lt;/code&gt; and &lt;code&gt;_flag&lt;/code&gt; parameters, several header injection paths, a CSRF bypass in the mark-as-junk action, an XSS in password driver error handling, and a bug that let the Modoboa password driver change another account's password. Several of these need a logged-in user, so the risk depends on how many accounts can sign in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find your real version
&lt;/h2&gt;

&lt;p&gt;The version is in &lt;code&gt;program/include/iniset.php&lt;/code&gt; as &lt;code&gt;RCMAIL_VERSION&lt;/code&gt;, and also shown under About in the settings menu if you have not hidden it. Compare against these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.6 branch: you need 1.6.20. Anything from 1.6.0 to 1.6.15 is also open to the exploited CVE-2026-48842.&lt;/li&gt;
&lt;li&gt;1.7 branch: you need 1.7.5. 1.7.0 is also open to CVE-2026-48842.&lt;/li&gt;
&lt;li&gt;1.5 or older: these releases do not cover it. Plan a move to 1.6 LTS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Roundcube came bundled with something else, such as a control panel, a mail server suite or a container image, the bundle's own version number tells you nothing. Check the Roundcube version inside it, and check whether the vendor has shipped 1.6.20 or 1.7.5 yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: update, then check the plugin list
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Back up the Roundcube database and the &lt;code&gt;config/&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Install 1.6.20 or 1.7.5 from the &lt;a href="https://github.com/roundcube/roundcubemail/releases" rel="noopener noreferrer"&gt;official GitHub releases&lt;/a&gt; or your distribution's package, and run the update script so the database schema matches.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;config/config.inc.php&lt;/code&gt; and look at the &lt;code&gt;plugins&lt;/code&gt; array. Remove anything you do not use. If &lt;code&gt;virtuser_query&lt;/code&gt; is there and you were below 1.6.16 or 1.7.1, treat the server as possibly compromised and go to step 3.&lt;/li&gt;
&lt;li&gt;Confirm that PHP itself is current. Roundcube is a PHP application, so the &lt;a href="https://fanpino.com/en/blog/php2shell-patch-php-docker-wordpress/" rel="noopener noreferrer"&gt;PHP2Shell flaws disclosed on 21 October&lt;/a&gt; apply to it too, and updating Roundcube does not update PHP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: if you ran an exposed version with virtuser_query
&lt;/h2&gt;

&lt;p&gt;A SQL injection against the Roundcube database can read what that database holds: user preferences, address books, identities and, depending on configuration, the session table. Practical checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search the web server logs for unusual characters, especially backslashes and SQL keywords, in login requests from before you patched.&lt;/li&gt;
&lt;li&gt;Look at the database user Roundcube connects with. If it has rights on other databases on the same server, assume those were reachable too, and reduce it to the Roundcube database only.&lt;/li&gt;
&lt;li&gt;Expire all sessions by clearing the session table after the update, and rotate the &lt;code&gt;des_key&lt;/code&gt; in the config.&lt;/li&gt;
&lt;li&gt;If the same database server holds mail-server user tables, rotate those credentials as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern behind both
&lt;/h2&gt;

&lt;p&gt;Webmail sits on the public internet and talks to IMAP, SMTP, a database, an image library and PHP, so a flaw in any of them is a flaw in the mail service. The Roundcube fixes this month are the third reminder in a few weeks, after the &lt;a href="https://fanpino.com/en/blog/zimbra-cve-2026-73570-compromise-check/" rel="noopener noreferrer"&gt;Zimbra SNMP command injection&lt;/a&gt; and PHP2Shell, that optional add-ons and the runtime underneath matter as much as the mail server itself. Enable only the plugins you use, keep the database account narrow, and track the runtime version, not just the application's. If you are rethinking the stack, our &lt;a href="https://fanpino.com/en/blog/mailcow-vs-stalwart-vs-mailu-production/" rel="noopener noreferrer"&gt;Mailcow, Stalwart and Mailu comparison&lt;/a&gt; covers how each one handles webmail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which Roundcube version fixes the actively exploited SQL injection?
&lt;/h3&gt;

&lt;p&gt;CVE-2026-48842 is fixed in Roundcube 1.6.16 and 1.7.1, released on 24 May 2026. The latest releases, 1.6.20 and 1.7.5 from 19 October 2026, include that fix plus 20 more security fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is CVE-2026-48842 exploitable on every Roundcube server?
&lt;/h3&gt;

&lt;p&gt;No. It is in the virtuser_query plugin, so a server is exposed only if that plugin is enabled and Roundcube is older than 1.6.16 on the 1.6 branch or 1.7.1 on the 1.7 branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are the October 2026 Roundcube vulnerabilities being exploited?
&lt;/h3&gt;

&lt;p&gt;The Roundcube project did not report exploitation of the 20 issues fixed in 1.6.20 and 1.7.5. The exploitation warning from Singapore's CSA on 3 October concerns the older CVE-2026-48842.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does updating Roundcube also protect against PHP2Shell?
&lt;/h3&gt;

&lt;p&gt;No. PHP2Shell is a flaw in the PHP runtime. Roundcube needs both its own update and a fixed PHP version, such as 8.2.35, 8.3.36, 8.4.27 or 8.5.12, or a distribution build with the backported fix.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://roundcube.net/news/2026/10/19/security-updates-1.6.20-and-1.7.5" rel="noopener noreferrer"&gt;Roundcube, 19 October 2026&lt;/a&gt;, &lt;a href="https://roundcube.net/news/2026/05/24/security-updates-1.6.16-and-1.7.1" rel="noopener noreferrer"&gt;Roundcube, 24 May 2026&lt;/a&gt;, &lt;a href="https://www.csa.gov.sg/alerts-and-advisories/alerts/al-2026-131/" rel="noopener noreferrer"&gt;CSA Singapore alert AL-2026-131&lt;/a&gt; and the NVD entries linked above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://fanpino.com/en/blog/roundcube-1-6-20-1-7-5-security-update-cve-2026-48842/" rel="noopener noreferrer"&gt;fanpino.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>email</category>
      <category>php</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Zimbra CVE-2026-73570: Patched in July? Check Whether Someone Got In First</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Sun, 01 Nov 2026 22:27:14 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/zimbra-cve-2026-73570-patched-in-july-check-whether-someone-got-in-first-2og</link>
      <guid>https://dev.to/hamzezarenasiri/zimbra-cve-2026-73570-patched-in-july-check-whether-someone-got-in-first-2og</guid>
      <description>&lt;p&gt;Zimbra fixed CVE-2026-73570 in version 10.1.20 on 20 July 2026. The flaw lets anyone who can send mail to a Zimbra server run shell commands on it, with no login and no user action, as long as the optional &lt;code&gt;zimbra-snmp&lt;/code&gt; package is installed and SNMP notifications are switched on. On 30 September &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/09/30/unauthenticated-command-injection-on-internet-facing-mail-servers-tracking-cve-2026-73570/" rel="noopener noreferrer"&gt;Microsoft Threat Intelligence published&lt;/a&gt; what attackers did with it: web shells, root access, stolen authentication keys and mailbox archives, across organisations in more than one region and industry.&lt;/p&gt;

&lt;p&gt;If you upgraded in July and never had SNMP notifications on, you can stop here. Everyone else has two jobs. The first is to patch. The second, which most advice skips, is to check whether someone got in before you did, because the upgrade closes the hole but does not remove a web shell, a sudoers entry or a stolen key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why patch dates matter more than usual here
&lt;/h2&gt;

&lt;p&gt;According to Microsoft, two separate scanning tools probed the vulnerable path between 28 July and 7 August. That was after the fix shipped on 20 July but before the public disclosure on 13 August. In other words, attackers were testing servers while most admins had no reason yet to rush. A server that took the update in late August was exposed during a window when the bug was already being worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the attack works, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Zimbra's monitoring daemon, &lt;code&gt;swatchdog&lt;/code&gt;, watches the mail log for "service status change" lines and, when SNMP notifications are enabled, sends an &lt;code&gt;snmptrap&lt;/code&gt; through a shell. &lt;a href="https://www.elastic.co/security-labs/threat-command/command-injection-zimbra-cve-2026-73570" rel="noopener noreferrer"&gt;Elastic Security Labs reproduced the chain&lt;/a&gt;: an attacker sends an SMTP command with a malformed address, Postfix writes it to &lt;code&gt;/var/log/zimbra.log&lt;/code&gt;, and if that text looks like a status line, the service name part reaches &lt;code&gt;/bin/sh&lt;/code&gt;. The result is code execution as the &lt;code&gt;zimbra&lt;/code&gt; user. The &lt;a href="https://nvd.nist.gov/vuln/detail/cve-2026-73570" rel="noopener noreferrer"&gt;CVE entry&lt;/a&gt; carries a CNA score of 8.9.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: patch, or remove the trigger
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade every Zimbra node to 10.1.20 or later. Microsoft and Elastic both call this the only complete fix.&lt;/li&gt;
&lt;li&gt;If you cannot upgrade today, Microsoft's interim advice is to uninstall the &lt;code&gt;zimbra-snmp&lt;/code&gt; package, disable SNMP notifications, and restrict SNMP and SMTP access to trusted hosts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not trust a quick exploit test as proof you are safe. Elastic found that several public proof-of-concept scripts write a payload that &lt;code&gt;swatchdog&lt;/code&gt; never matches, so a failed test can make a vulnerable server look patched. Check the installed version instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: look for what attackers left behind
&lt;/h2&gt;

&lt;p&gt;These checks come straight from the behaviour Microsoft describes. Run them on every mailbox and MTA node, not only the one facing the internet, because the attackers moved between nodes using Zimbra's own SSH key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web shells.&lt;/strong&gt; Look for JSP files that do not belong to your Zimbra version in the Jetty and mailboxd web application directories, including servlet work directories. Microsoft saw several copies per server, plus copies on peer nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sudo access for the zimbra account.&lt;/strong&gt; Run &lt;code&gt;sudo -l -U zimbra&lt;/code&gt; and search &lt;code&gt;/etc/sudoers&lt;/code&gt; and &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; for &lt;code&gt;NOPASSWD: ALL&lt;/code&gt;. The attackers created exactly that entry by briefly taking ownership of &lt;code&gt;/etc/pam.d/sudo&lt;/code&gt;, then restored the PAM file but kept the sudoers line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fake systemd service.&lt;/strong&gt; Check for &lt;code&gt;/etc/systemd/system/zimlog.service&lt;/code&gt;. The name sounds like part of Zimbra, but Microsoft found it installed by hand in the system-wide systemd directory, outside Zimbra's own folders, and the attackers changed its timestamps to match older system services, so sorting by date will not reveal it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron jobs and odd processes.&lt;/strong&gt; Review crontabs for the zimbra user and root, and look for a named pipe at &lt;code&gt;/tmp/s&lt;/code&gt; or an &lt;code&gt;openssl s_client&lt;/code&gt; process wired to a shell, which Microsoft observed as an encrypted reverse shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log evidence.&lt;/strong&gt; Search &lt;code&gt;/var/log/zimbra.log&lt;/code&gt; for "Illegal address syntax" lines that contain &lt;code&gt;$(&lt;/code&gt;, backticks or the text "Service status change", and web logs for the &lt;code&gt;ZB73570&lt;/code&gt; User-Agent used by the early probes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: if you find anything, rotate the keys, not just passwords
&lt;/h2&gt;

&lt;p&gt;Microsoft says the attackers went after Zimbra's central secrets rather than individual passwords. They ran &lt;code&gt;zmlocalconfig -s&lt;/code&gt;, which prints the LDAP, MySQL, Postfix, Amavis and replication credentials, then used LDAP to pull &lt;code&gt;zimbraPreAuthKey&lt;/code&gt;, &lt;code&gt;zimbraAuthTokenKey&lt;/code&gt; and &lt;code&gt;zimbraTwoFactorAuthSecret&lt;/code&gt;. With the pre-auth or auth-token key, an attacker can sign in as any user without knowing their password, so resetting user passwords alone does nothing. On a server with confirmed compromise, plan to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rotate the service credentials shown by &lt;code&gt;zmlocalconfig -s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Regenerate the pre-auth keys for each domain and the auth token key, so tokens signed with the stolen keys stop working.&lt;/li&gt;
&lt;li&gt;Re-enrol two-factor authentication, since the shared secrets may have been copied.&lt;/li&gt;
&lt;li&gt;Replace the &lt;code&gt;zimbra&lt;/code&gt; SSH key in &lt;code&gt;/opt/zimbra/.ssh/&lt;/code&gt; on every node.&lt;/li&gt;
&lt;li&gt;Treat mailbox contents as read. Microsoft observed archives being created and transferred out, which may trigger breach notification duties depending on where you operate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In many cases rebuilding the affected nodes from a clean install and restoring data is quicker and more certain than cleaning them, given that the attackers had root and edited timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wider lesson for self-hosted mail
&lt;/h2&gt;

&lt;p&gt;This bug sat in an optional monitoring add-on, not in the mail path most admins think about. It is the same pattern as the &lt;a href="https://fanpino.com/en/blog/citrix-netscaler-zero-days-rotate-secrets-after-patching/" rel="noopener noreferrer"&gt;NetScaler zero-days&lt;/a&gt;: the patch closes the door, but secrets taken before the patch stay valid until you rotate them. Two habits help on any self-hosted mail server. Install only the packages you use, and keep a written list of every key the server holds, so that rotating them after an incident is a checklist rather than a research project. If you are comparing self-hosted platforms, our &lt;a href="https://fanpino.com/en/blog/mailcow-vs-stalwart-vs-mailu-production/" rel="noopener noreferrer"&gt;Mailcow, Stalwart and Mailu comparison&lt;/a&gt; covers how each one is put together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is my Zimbra server affected by CVE-2026-73570?
&lt;/h3&gt;

&lt;p&gt;It is affected if it runs a version before 10.1.20, has the optional zimbra-snmp package installed and has SNMP notifications enabled. Servers without SNMP notifications are not exposed to this path, but should still be upgraded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is CVE-2026-73570 being exploited?
&lt;/h3&gt;

&lt;p&gt;Yes. Microsoft Threat Intelligence reported confirmed compromises in several regions and industries, and probing of the vulnerable path from 28 July 2026, before public disclosure on 13 August.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does upgrading to 10.1.20 remove an existing compromise?
&lt;/h3&gt;

&lt;p&gt;No. The upgrade fixes the flaw, but web shells, sudoers entries, systemd units and stolen keys remain. Check every node and rotate Zimbra's authentication keys if you find signs of access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I test my server with a public exploit?
&lt;/h3&gt;

&lt;p&gt;It is not a reliable test. Elastic Security Labs found that several public proofs of concept never trigger the vulnerable code, so a failed test can hide a vulnerable server. Check the installed version and SNMP settings instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/09/30/unauthenticated-command-injection-on-internet-facing-mail-servers-tracking-cve-2026-73570/" rel="noopener noreferrer"&gt;Microsoft Threat Intelligence&lt;/a&gt; (30 September 2026), &lt;a href="https://www.elastic.co/security-labs/threat-command/command-injection-zimbra-cve-2026-73570" rel="noopener noreferrer"&gt;Elastic Security Labs&lt;/a&gt; (23 October 2026) and &lt;a href="https://nvd.nist.gov/vuln/detail/cve-2026-73570" rel="noopener noreferrer"&gt;NVD CVE-2026-73570&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://fanpino.com/en/blog/zimbra-cve-2026-73570-compromise-check/" rel="noopener noreferrer"&gt;fanpino.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>email</category>
      <category>linux</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>PHP2Shell: Updating WordPress Won't Patch It. Here's How to Patch the PHP Underneath</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:41:53 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/php2shell-updating-wordpress-wont-patch-it-heres-how-to-patch-the-php-underneath-2f77</link>
      <guid>https://dev.to/hamzezarenasiri/php2shell-updating-wordpress-wont-patch-it-heres-how-to-patch-the-php-underneath-2f77</guid>
      <description>&lt;p&gt;Liquid syntax error: Unknown tag 'endraw'&lt;/p&gt;
</description>
      <category>php</category>
      <category>security</category>
      <category>docker</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>MCP Server Audit Logging: What to Log So You Can Answer "Were We Affected?"</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:42:47 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/mcp-server-audit-logging-what-to-log-so-you-can-answer-were-we-affected-558i</link>
      <guid>https://dev.to/hamzezarenasiri/mcp-server-audit-logging-what-to-log-so-you-can-answer-were-we-affected-558i</guid>
      <description>&lt;p&gt;When the Deadbugz campaign was published in August 2026, the first question most teams asked was "were we affected?" Most could not answer it. The malicious MCP server behaved like a text formatter for its first three tool calls and only then changed the instructions it sent to the agent. A team that had logged nothing but "server connected" had no record of what the server said, what the agent did next, or which files it touched. This guide is the logging setup we wish we had had before our own &lt;a href="https://fanpino.com/en/blog/malicious-mcp-servers-deadbugz-audit-checklist/" rel="noopener noreferrer"&gt;MCP audit&lt;/a&gt;: what to record, where, and how long to keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should an MCP audit log contain?
&lt;/h2&gt;

&lt;p&gt;An MCP audit log should record every server launch with its exact command and version, every tool definition the server sends and any change to it, every tool call with its arguments and result size, the identity and token scopes behind each call, and outbound network requests. Together these let you rebuild what an agent did and who it acted for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ordinary application logs miss MCP attacks
&lt;/h2&gt;

&lt;p&gt;An MCP server has two channels into your systems. It runs code, and it sends the agent text, such as tool names, descriptions and prompt templates, that the model treats as instructions. Most logging only covers the first channel, if that. The Deadbugz payload arrived through the second: new instructions in later tools/list and prompts/get responses, after a counter reached three calls. Nothing at install time looked wrong. Only a log that stores what the server said, and when it changed, would have shown it.&lt;/p&gt;

&lt;p&gt;The protocol's own &lt;a href="https://modelcontextprotocol.io/specification/draft/basic/security_best_practices" rel="noopener noreferrer"&gt;security best practices&lt;/a&gt; point the same way in several places. Proxy services that spawn local servers should "log all stdio transport usage for security monitoring". Servers should log scope elevation events "with correlation IDs". Clients should "log suspicious authorization URLs". And the document warns that token passthrough breaks the audit trail, because the server can no longer tell clients apart and downstream logs show the wrong identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The events worth logging
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Fields to record&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server launch&lt;/td&gt;
&lt;td&gt;Full command and arguments, package version or commit hash, config file it came from, user it runs as&lt;/td&gt;
&lt;td&gt;Unpinned launches, servers added by a pull request, a changed command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool definitions&lt;/td&gt;
&lt;td&gt;Hash and full text of each tools/list and prompts/list response, per session, with a timestamp&lt;/td&gt;
&lt;td&gt;Descriptions that change after approval or after a few calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool call&lt;/td&gt;
&lt;td&gt;Tool name, server, arguments (secrets redacted), result size, duration, error&lt;/td&gt;
&lt;td&gt;A formatter that suddenly reads files, unusual argument patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity and scopes&lt;/td&gt;
&lt;td&gt;Human user, agent session ID, token audience and scopes, correlation ID&lt;/td&gt;
&lt;td&gt;Over-broad tokens, token passthrough, calls nobody can attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope elevation&lt;/td&gt;
&lt;td&gt;Scope requested, subset granted, which tool triggered it&lt;/td&gt;
&lt;td&gt;Privilege creeping up during a session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound requests&lt;/td&gt;
&lt;td&gt;Destination host, port, bytes sent, which server or tool made it&lt;/td&gt;
&lt;td&gt;Quiet exfiltration, calls to metadata addresses like 169.254.169.254&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File access outside the project&lt;/td&gt;
&lt;td&gt;Path, read or write, process&lt;/td&gt;
&lt;td&gt;Reads of ~/.ssh, cloud credential files, shell history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Log tool definitions, not just tool calls
&lt;/h2&gt;

&lt;p&gt;This is the one most teams skip. Save the full output of tools/list and prompts/list each time a session starts, and again after several calls, then compare hashes. A changed description on a server you already approved should raise an alert and need a person's approval again; that is the recommendation Pillar Security gave MCP client builders after Deadbugz. Storing the full text, not only the hash, matters for the investigation afterwards, because the changed description is the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the logs should live
&lt;/h2&gt;

&lt;p&gt;Not on the same machine as the agent, and not anywhere the agent can write. If an agent can be steered into deleting files, it can be steered into deleting its own log. Ship the events off the host as they happen. A practical pattern: have the MCP client or a small local proxy write one JSON line per event to stdout, and let your existing log shipper collect it, the same way &lt;a href="https://fanpino.com/en/blog/container-logs-stdout-not-files/" rel="noopener noreferrer"&gt;container logs should go to stdout, not files&lt;/a&gt;. Network events come more reliably from an egress proxy than from the agent itself, since a compromised process will not report its own exfiltration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redact before you store
&lt;/h2&gt;

&lt;p&gt;Tool arguments and results carry exactly the things an attacker wants: tokens, file contents, customer records. Redact known secret patterns before writing, store result sizes instead of full results by default, and keep full payloads only for tools you have marked sensitive, in a store with tighter access. An audit log that leaks credentials becomes the next incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long to keep MCP logs
&lt;/h2&gt;

&lt;p&gt;Keep them at least as long as your other security logs, and long enough to cover the gap between an attack and its public disclosure. Deadbugz pull requests were opened on 10 August and the write-up came out on 12 August; other campaigns surface months later. If your incident response policy already sets a retention period for authentication and endpoint logs, use the same one rather than inventing a shorter one for AI tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal setup for a small team
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Inventory every MCP server and pin its version, as in our &lt;a href="https://fanpino.com/en/blog/malicious-mcp-servers-deadbugz-audit-checklist/" rel="noopener noreferrer"&gt;audit checklist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Wrap launches so the exact command, version and user are logged once per session.&lt;/li&gt;
&lt;li&gt;Snapshot tools/list and prompts/list at session start and after the third call, and alert on any hash change.&lt;/li&gt;
&lt;li&gt;Log every tool call with redacted arguments and result size, tagged with the human user and session ID.&lt;/li&gt;
&lt;li&gt;Route agent traffic through an allow-listed egress proxy and keep its logs off the host.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same principle shapes how we build internal assistants: an assistant should not hold more power than the person it works for, and every answer should be traceable to a source. We covered the access side in &lt;a href="https://fanpino.com/en/blog/internal-ai-assistant-access-control-citations-2026/" rel="noopener noreferrer"&gt;scoping an internal AI assistant to the person asking&lt;/a&gt;. In &lt;a href="https://fanpino.com/en/showcase/fanmind/" rel="noopener noreferrer"&gt;FanMind&lt;/a&gt;, our document assistant, outbound calls go through one allow-listed tunnel and confidential documents are excluded from AI indexing by default, which keeps both the attack surface and the log volume small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What should I log for MCP servers?
&lt;/h3&gt;

&lt;p&gt;Log each server launch with its exact command, version and user, the full text and hash of every tools/list and prompts/list response, every tool call with redacted arguments and result size, the human user, session ID and token scopes behind each call, scope elevation events, and outbound network requests made by the server or agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why log MCP tool definitions?
&lt;/h3&gt;

&lt;p&gt;Because tool names, descriptions and prompt templates are instructions the model follows, and they can change after you approve a server. The Deadbugz server only changed what it sent after three tool calls. Comparing snapshots of tools/list and prompts/list over a session is the only way to see that change and prove what the agent was told.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the MCP specification require audit logging?
&lt;/h3&gt;

&lt;p&gt;It does not define one audit log format, but its security best practices recommend logging in several places: proxies should log all stdio transport usage for security monitoring, servers should log scope elevation events with correlation IDs, and clients should log suspicious authorization URLs. It also warns that token passthrough damages the audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where should MCP audit logs be stored?
&lt;/h3&gt;

&lt;p&gt;Off the host where the agent runs and out of the agent's write access, so a steered agent cannot delete them. Emit events as JSON lines to stdout and ship them with your existing log pipeline, and take network events from an egress proxy, because a compromised process will not reliably report its own outbound traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/draft/basic/security_best_practices" rel="noopener noreferrer"&gt;Model Context Protocol: Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pillar.security/blog/deadbugz-currently-active-mcp-supply-chain-campaign" rel="noopener noreferrer"&gt;Pillar Security: Deadbugz, an active MCP supply-chain campaign (12 August 2026)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>mcp</category>
      <category>logging</category>
    </item>
    <item>
      <title>Exchange's Last ESU Patch Is Out, and a Windows Update Is Knocking Servers Over</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:34:41 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/exchanges-last-esu-patch-is-out-and-a-windows-update-is-knocking-servers-over-136f</link>
      <guid>https://dev.to/hamzezarenasiri/exchanges-last-esu-patch-is-out-and-a-windows-update-is-knocking-servers-over-136f</guid>
      <description>&lt;p&gt;On 13 October 2026 Microsoft shipped what it calls the last security update that Exchange Server 2016 and 2019 will ever get. Three days later the Exchange team published a second post: some Exchange servers were becoming unstable, and the cause was not the Exchange update at all but that month's Windows Server patches. If you run Exchange on your own hardware, both posts land on your desk in the same week. &lt;/p&gt;

&lt;p&gt;We wrote about the end-of-support dates and the three ways out in &lt;a href="https://fanpino.com/en/blog/exchange-2019-end-of-support-self-hosted-migration/" rel="noopener noreferrer"&gt;our Exchange 2016/2019 exit plan&lt;/a&gt;. This is the shorter, more urgent follow-up: what the October release fixes, how to handle the Windows problem, and what changes from November. &lt;/p&gt;

&lt;h2&gt;
  
  
  What the October 2026 release covers
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://techcommunity.microsoft.com/blog/exchange/released-october-2026-exchange-server-security-updates/4563014" rel="noopener noreferrer"&gt;Exchange team's release post&lt;/a&gt;, the updates are available for Exchange Server Subscription Edition (SE) RTM, Exchange 2019 CU14 and CU15, and Exchange 2016 CU23. The 2016 and 2019 builds are only available to organisations enrolled in the Period 2 Extended Security Update (ESU) program. Exchange Online customers are already protected, but the post is clear that hybrid setups still need the update on every on-premises Exchange server and on any workstation running the Exchange Management Tools. &lt;/p&gt;

&lt;p&gt;Microsoft's October security release lists seven Exchange Server CVEs. The one to read first is &lt;a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-70571" rel="noopener noreferrer"&gt;CVE-2026-70571&lt;/a&gt;, a stack-based buffer overflow that Microsoft rates Critical, with a CVSS score of 8.1, allowing an unauthorised attacker to run code over the network. Next is &lt;a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-70279" rel="noopener noreferrer"&gt;CVE-2026-70279&lt;/a&gt;, a cross-site scripting flaw scored 9.3 that an unauthenticated attacker can use for spoofing. Microsoft marked both, along with the elevation-of-privilege bug &lt;a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-96940" rel="noopener noreferrer"&gt;CVE-2026-96940&lt;/a&gt;, as "Exploitation More Likely". None of the seven was listed as publicly disclosed or exploited at release. That is the good news, and it usually does not last long for Exchange. &lt;/p&gt;

&lt;p&gt;SUs are cumulative. If your server is on a supported CU, you install only the latest SU, not every one you missed. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Windows update that is taking Exchange down
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://techcommunity.microsoft.com/blog/exchange/exchange-servers-may-become-unstable-after-installation-of-october-2026-windows-/4565382" rel="noopener noreferrer"&gt;16 October post&lt;/a&gt; describes servers that run fine for a while after patching and then degrade. Reported symptoms include OWA failing with &lt;code&gt;MapiExceptionMdbOffline&lt;/code&gt; and event ID 1006 in the Application log, mail queues growing through the day until delivery almost stops, and DAG members failing to reach quorum. A reboot helps, then the problem comes back. &lt;/p&gt;

&lt;p&gt;Microsoft names four Windows Server updates as the trigger:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KB5126646 on Windows Server 2025 &lt;/li&gt;
&lt;li&gt;KB5126655 on Windows Server 2022 &lt;/li&gt;
&lt;li&gt;KB5126651 on Windows Server 2019 &lt;/li&gt;
&lt;li&gt;KB5126868 on Windows Server 2016 &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any Exchange version on those Windows builds can be affected, SE included. Busy servers show it sooner. Some may never show it. &lt;/p&gt;

&lt;p&gt;Microsoft's advice is not to uninstall the Windows updates. Instead, from an elevated command prompt on the Exchange server, run: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;reg add "HKLM\SOFTWARE\Microsoft\Rpc" /v EpmapSavedContextCap /t REG_DWORD /d 65536 /f&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;Then reboot and watch. If the problem returns, Microsoft says to raise the limit. In practice that means you should schedule the registry change in the same maintenance window as the October patches, rather than waiting for the help desk to report that Outlook cannot connect. &lt;/p&gt;

&lt;h2&gt;
  
  
  A patch-week checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run Microsoft's Exchange Server Health Checker on every server, as the release post recommends, to see which CU and SU each one is on. &lt;/li&gt;
&lt;li&gt;Bring each server to a CU the October SU supports before installing it. &lt;/li&gt;
&lt;li&gt;Install the October Windows update and the October Exchange SU in the same window, then apply the RPC registry value. &lt;/li&gt;
&lt;li&gt;After the reboot, confirm every Exchange service has started. Disabled services usually mean an interrupted setup. &lt;/li&gt;
&lt;li&gt;For the next few days, watch queue length and OWA errors closely, not just "server up". &lt;/li&gt;
&lt;li&gt;Patch the management-tools workstations too. They are easy to forget. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  From November, 2016 and 2019 go quiet
&lt;/h2&gt;

&lt;p&gt;The release post spells out what happens next. After October 2026, updates will be released for Exchange SE only, and Microsoft's announcements and CVE entries will stop mentioning 2016 and 2019. The team adds that this does not mean those versions are safe from future bugs. Microsoft will simply stop testing them. Buying Period 2 ESU late only brings you up to this October release. &lt;/p&gt;

&lt;p&gt;So from the November Patch Tuesday onward, an Exchange 2019 server will be exposed to every new Exchange flaw with no fix and no CVE that even names your version. For organisations in the UAE and Saudi Arabia that kept mail on their own servers for data-residency reasons, the cloud route may not be acceptable either. We looked at those rules in &lt;a href="https://fanpino.com/en/blog/helpdesk-data-residency-uae-saudi-pdpl/" rel="noopener noreferrer"&gt;our data-residency guide&lt;/a&gt;. That leaves two real paths: an in-place upgrade to Exchange SE, or a move to another mail server you host yourself. &lt;/p&gt;

&lt;h2&gt;
  
  
  If you are weighing the self-hosted route
&lt;/h2&gt;

&lt;p&gt;Open-source mail servers have matured a lot. We compared the main candidates under real load in &lt;a href="https://fanpino.com/en/blog/mailcow-vs-stalwart-vs-mailu-production/" rel="noopener noreferrer"&gt;Mailcow vs Stalwart vs Mailu&lt;/a&gt;. Our own product, &lt;a href="https://fanpino.com/en/showcase/fanmail/" rel="noopener noreferrer"&gt;FanMail&lt;/a&gt;, is built on Stalwart Mail Server and runs fully on your own infrastructure, with a JMAP webmail, calendar, contacts and a built-in admin panel. It has replaced a commercial suite at 10,000-mailbox scale. It is not a drop-in Exchange clone: Outlook users who depend on every Exchange-specific feature should test with real mailboxes before committing. &lt;/p&gt;

&lt;p&gt;Whichever way you go, this month's job is the same. Patch, add the registry fix, and put a date on the decision before November's patches arrive without you. &lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is October 2026 really the last update for Exchange 2016 and 2019?
&lt;/h3&gt;

&lt;p&gt;Yes. Microsoft's release post says October 2026 is the last month Period 2 ESU customers receive new Exchange 2016 and 2019 security updates, and that later updates will be for Exchange SE only. &lt;/p&gt;

&lt;h3&gt;
  
  
  Should we uninstall the October Windows update if Exchange becomes unstable?
&lt;/h3&gt;

&lt;p&gt;No. Microsoft recommends keeping the Windows update and setting the EpmapSavedContextCap registry value under HKLM\SOFTWARE\Microsoft\Rpc, then rebooting. &lt;/p&gt;

&lt;h3&gt;
  
  
  Are any of the October Exchange vulnerabilities being exploited?
&lt;/h3&gt;

&lt;p&gt;Not at release. Microsoft listed none of the seven Exchange CVEs as exploited or publicly disclosed, but rated three as Exploitation More Likely, including the Critical remote code execution bug CVE-2026-70571. &lt;/p&gt;

&lt;h3&gt;
  
  
  We use Exchange Online with a hybrid server. Do we need to act?
&lt;/h3&gt;

&lt;p&gt;Yes. Exchange Online is protected, but Microsoft says the SU must still be installed on on-premises Exchange servers, even ones used only for management. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://techcommunity.microsoft.com/blog/exchange/released-october-2026-exchange-server-security-updates/4563014" rel="noopener noreferrer"&gt;Released: October 2026 Exchange Server Security Updates&lt;/a&gt; (Exchange Team, 13 October 2026) and &lt;a href="https://techcommunity.microsoft.com/blog/exchange/exchange-servers-may-become-unstable-after-installation-of-october-2026-windows-/4565382" rel="noopener noreferrer"&gt;Exchange servers may become unstable after installation of October 2026 Windows updates&lt;/a&gt; (Exchange Team, 16 October 2026).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://fanpino.com/en/blog/exchange-october-2026-last-esu-update-windows-instability/" rel="noopener noreferrer"&gt;fanpino.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>exchange</category>
      <category>security</category>
      <category>sysadmin</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>One Wildcard Certificate, Several Servers: Renewal That Doesn't Break</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:12:54 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/one-wildcard-certificate-several-servers-renewal-that-doesnt-break-34lf</link>
      <guid>https://dev.to/hamzezarenasiri/one-wildcard-certificate-several-servers-renewal-that-doesnt-break-34lf</guid>
      <description>&lt;p&gt;A wildcard certificate on one server is easy. You switch to the DNS-01 challenge, certbot renews it every couple of months, and you forget it exists. The trouble starts when the same &lt;code&gt;*.example.com&lt;/code&gt; has to live on a second box, then a NAS, then a cloud VM in front of everything. Renewal works on one machine and quietly fails on the others, and you find out when a browser shows a red warning.&lt;/p&gt;

&lt;p&gt;We run a handful of servers behind one domain, including a mail server, and went through this. This piece covers why per-host renewal breaks, the single-issuer pattern that keeps working, and the certbot hook detail that decides whether your script runs at the right moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why each server renewing on its own goes wrong
&lt;/h2&gt;

&lt;p&gt;Nothing in Let's Encrypt stops several hosts from requesting the same wildcard. The problems are around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits.&lt;/strong&gt; Let's Encrypt allows &lt;a href="https://letsencrypt.org/docs/rate-limits/" rel="noopener noreferrer"&gt;up to 5 certificates per exact same set of identifiers every 7 days&lt;/a&gt;, and the limit is global: orders from every account count. Three servers, each reinstalled or tested a couple of times, can use that up in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge delegation.&lt;/strong&gt; This is the one behind "only the last server renews". With &lt;a href="https://github.com/joohoi/acme-dns" rel="noopener noreferrer"&gt;acme-dns&lt;/a&gt;, each registration returns its own random subdomain, and you point &lt;code&gt;_acme-challenge.example.com&lt;/code&gt; at it with a CNAME. A name can have only one CNAME. When the third server registers and you update the record, the first two keep writing TXT values to subdomains the CA no longer looks at. Their renewals fail with a DNS validation error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS credentials everywhere.&lt;/strong&gt; Every host that renews needs a token that can write to your zone. Three hosts means three copies of a key that can change any record you own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP-01 does not scale either.&lt;/strong&gt; Behind a home router or one public IP, port 80 can forward to only one machine. On one of our networks inbound port 80 was filtered upstream, and HTTP-01 renewals failed for weeks before anyone noticed. DNS-01 does not depend on any inbound port, which is why it is the right base for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: one issuer, then push
&lt;/h2&gt;

&lt;p&gt;Pick one machine to own the certificate. It holds the DNS API token, runs certbot or acme.sh, and is the only host that talks to the CA. Every other server just receives files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One renewal schedule to watch instead of three.&lt;/li&gt;
&lt;li&gt;One DNS token, on one machine.&lt;/li&gt;
&lt;li&gt;Rate limits stop being a concern, because one host renews one certificate.&lt;/li&gt;
&lt;li&gt;Adding a fifth server is one more line in a script, not another ACME setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receiving servers need a fixed path for the files, a user that can write there, and permission to reload their own web or mail server. Nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the deploy hook, not the post hook
&lt;/h2&gt;

&lt;p&gt;Certbot has three hook types, and the &lt;a href="https://eff-certbot.readthedocs.io/en/stable/using.html" rel="noopener noreferrer"&gt;certbot documentation&lt;/a&gt; is specific about when each one runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;When certbot runs it&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--pre-hook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before each renewal attempt, only when a cert is due&lt;/td&gt;
&lt;td&gt;Stopping a service that holds port 80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--post-hook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After each renewal attempt, including failed ones&lt;/td&gt;
&lt;td&gt;Starting that service again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--deploy-hook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only after a certificate was actually renewed&lt;/td&gt;
&lt;td&gt;Copying the new files and reloading servers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hooks only fire when a certificate is due for renewal, so the cron or systemd timer can run &lt;code&gt;certbot renew&lt;/code&gt; often without side effects. The difference is in what happens when renewal fails. A post hook runs anyway. If your copy script lives there, a failed renewal still pushes the old files out and reloads every server, and your logs say "deployed" while the certificate gets closer to expiry. A deploy hook runs only after a successful renewal.&lt;/p&gt;

&lt;p&gt;The deploy hook also gets two variables: &lt;code&gt;$RENEWED_LINEAGE&lt;/code&gt;, the live directory with the new files (for example &lt;code&gt;/etc/letsencrypt/live/example.com&lt;/code&gt;), and &lt;code&gt;$RENEWED_DOMAINS&lt;/code&gt;, the names on the renewed certificate. Any executable file in &lt;code&gt;/etc/letsencrypt/renewal-hooks/deploy/&lt;/code&gt; runs as a deploy hook, so you do not have to remember the flag.&lt;/p&gt;

&lt;p&gt;A minimal version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# /etc/letsencrypt/renewal-hooks/deploy/push-certs.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;web2 nas vm-proxy&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;rsync &lt;span class="nt"&gt;-aL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RENEWED_LINEAGE&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"certsync@&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;:/etc/ssl/wildcard/"&lt;/span&gt;
  ssh &lt;span class="s2"&gt;"certsync@&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'sudo nginx -t &amp;amp;&amp;amp; sudo systemctl reload nginx'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter. &lt;code&gt;rsync -L&lt;/code&gt; copies the real files, because the &lt;code&gt;live/&lt;/code&gt; directory holds symlinks into &lt;code&gt;archive/&lt;/code&gt;. The &lt;code&gt;certsync&lt;/code&gt; user on each target should have an SSH key limited to that job, and sudo rights only for the nginx test and reload. And &lt;code&gt;nginx -t&lt;/code&gt; runs before the reload, so a broken config on one host does not take the site down.&lt;/p&gt;

&lt;p&gt;Mail servers need the same treatment. Postfix, Dovecot and most others load the certificate at start, so they keep serving the old one until reloaded. If your mail server also uses the wildcard, add its reload command to the loop. We covered the mail stack side in &lt;a href="https://fanpino.com/en/blog/mailcow-vs-stalwart-vs-mailu-production/" rel="noopener noreferrer"&gt;Mailcow vs Stalwart vs Mailu in production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it without waiting two months
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;certbot renew --dry-run -v&lt;/code&gt; on the issuer first. It talks to the staging server and shows the real challenge error if there is one. The full log is in &lt;code&gt;/var/log/letsencrypt/letsencrypt.log&lt;/code&gt;. One catch: according to the certbot docs, deploy hooks do not run during a dry run unless you add &lt;code&gt;--run-deploy-hooks&lt;/code&gt;. Without it you have tested the renewal but not the distribution.&lt;/p&gt;

&lt;p&gt;Then check what each server actually serves, not what is on its disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; web2.example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; web2.example.com 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the dates differ between hosts, the copy or the reload failed on that one. Put that check in your monitoring with an alert at 14 days left. It costs nothing and catches the failures the hook cannot see. If the hosts run in containers, send the hook output to stdout so it ends up with the rest of your logs, as described in &lt;a href="https://fanpino.com/en/blog/container-logs-stdout-not-files/" rel="noopener noreferrer"&gt;why container logs belong on stdout&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shorter certificates make this mandatory
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt has &lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" rel="noopener noreferrer"&gt;announced the timeline&lt;/a&gt; for shorter lifetimes, in line with the CA/Browser Forum Baseline Requirements. On 10 February 2027 the default profile moves from 90-day to 64-day certificates. On 16 February 2028 it moves to 45 days, and the time a domain validation can be reused drops from 30 days to 7 hours. The opt-in &lt;code&gt;tlsserver&lt;/code&gt; profile was scheduled to issue 45-day certificates from 13 May 2026.&lt;/p&gt;

&lt;p&gt;A setup where someone copies a renewed certificate to the other servers by hand works, with some luck, at 90 days. At 45 days it fails. A single issuer with a deploy hook keeps working no matter how short the lifetime gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One host requests and renews the wildcard. The DNS token exists only there.&lt;/li&gt;
&lt;li&gt;If you use acme-dns, exactly one registration and one CNAME per name.&lt;/li&gt;
&lt;li&gt;The copy and reload script lives in &lt;code&gt;renewal-hooks/deploy/&lt;/code&gt;, not in a post hook.&lt;/li&gt;
&lt;li&gt;Restricted SSH key per target, sudo only for test and reload.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nginx -t&lt;/code&gt; (or the mail server's check) before every reload.&lt;/li&gt;
&lt;li&gt;Dry run with &lt;code&gt;--run-deploy-hooks&lt;/code&gt; once after setup.&lt;/li&gt;
&lt;li&gt;External expiry check on every hostname, alert at 14 days.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can every server request its own copy of the same wildcard certificate?
&lt;/h3&gt;

&lt;p&gt;Yes, but each copy counts against Let's Encrypt's limit of 5 certificates per exact set of names every 7 days, across all accounts. With three or four servers and a few test runs you can hit it. One issuer avoids the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does renewal work only on the server I set up last?
&lt;/h3&gt;

&lt;p&gt;The usual cause is challenge delegation. With acme-dns, each registration gives you a new subdomain, and the _acme-challenge CNAME can point to only one of them. The servers registered earlier write their TXT records to a name the CA never checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does certbot renew --dry-run test my deploy hook?
&lt;/h3&gt;

&lt;p&gt;No. The certbot documentation says deploy hooks do not run during a dry run unless you add --run-deploy-hooks. Pre and post hooks do run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to change anything for 45-day certificates?
&lt;/h3&gt;

&lt;p&gt;If renewal and distribution are fully automated, no. If any step is manual, yes: Let's Encrypt's default profile moves to 64-day certificates on 10 February 2027 and to 45 days on 16 February 2028, so a manual copy every two months stops being enough.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>letsencrypt</category>
      <category>nginx</category>
      <category>linux</category>
    </item>
    <item>
      <title>A bot enumerated every payment URL on our site and our logs never saw it</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:22:36 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/a-bot-enumerated-every-payment-url-on-our-site-and-our-logs-never-saw-it-28ai</link>
      <guid>https://dev.to/hamzezarenasiri/a-bot-enumerated-every-payment-url-on-our-site-and-our-logs-never-saw-it-28ai</guid>
      <description>&lt;p&gt;We migrated a customer panel's nginx container last week for an unrelated SEO fix (a trailing-slash redirect bug), rebuilt it, and redeployed. Routine stuff.&lt;/p&gt;

&lt;p&gt;A few days later I was pulling GA4 numbers to check on organic traffic and noticed something odd: roughly a quarter of our "Direct" channel sessions were landing on &lt;code&gt;/app/customer/login?returnTo=/payment/&amp;lt;plan&amp;gt;/&amp;lt;period&amp;gt;&lt;/code&gt; for a huge spread of plan slugs - not two or three, but nearly every pricing tier across every product we sell. Same browser fingerprint every time: &lt;code&gt;(not set)&lt;/code&gt;, desktop, Germany. Zero engagement on all of them.&lt;/p&gt;

&lt;p&gt;That's not a customer. That's a script walking through every checkout URL it could find, probably scraped off our own pricing page.&lt;/p&gt;

&lt;p&gt;I went to pull the nginx access log to find the source IP and got nothing. The container had been rebuilt days earlier for that unrelated SEO fix, and &lt;code&gt;access_log&lt;/code&gt; was pointed at a file inside the container (&lt;code&gt;/var/log/nginx/access.log&lt;/code&gt;), not stdout. The rebuild wiped it. &lt;code&gt;docker logs&lt;/code&gt; showed nothing because there was nothing on stdout to show. Our Promtail/Loki pipeline, which only scrapes the Docker logging driver, had never seen a single line of it either.&lt;/p&gt;

&lt;p&gt;The bot's IP is gone. There's no way to get it back now.&lt;/p&gt;

&lt;p&gt;The actual fix was two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/dev/stdout&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;error_log&lt;/span&gt; &lt;span class="n"&gt;/dev/stderr&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nginx treats &lt;code&gt;/dev/stdout&lt;/code&gt; and &lt;code&gt;/dev/stderr&lt;/code&gt; as regular files it can write to, so this isn't some special integration, it just means the same log lines that used to sit in a file now go through the container's stdout/stderr, which &lt;code&gt;docker logs&lt;/code&gt; already reads and which our existing Promtail config was already scraping. Nothing else changed.&lt;/p&gt;

&lt;p&gt;The lesson wasn't really about the bot. Low-rate GET enumeration of public pricing URLs isn't a very interesting threat on its own. It was that we had a monitoring pipeline that looked complete (Promtail, Loki, Grafana, the works) with one container quietly opting out of it, because its base nginx config still defaulted to writing log files the way it would on a bare-metal box from ten years ago. If this container gets rebuilt again next month, whatever it's doing then, we'll actually be able to see it.&lt;/p&gt;

&lt;p&gt;If you're running nginx in Docker and haven't checked, it's worth a look at your own &lt;code&gt;access_log&lt;/code&gt;/&lt;code&gt;error_log&lt;/code&gt; directives. A monitoring stack is only as complete as the containers that actually write to it.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>The Docker Compose Restart Policy Gotcha That Cost Us Two Weeks of Silent Downtime</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:26:50 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/the-docker-compose-restart-policy-gotcha-that-cost-us-two-weeks-of-silent-downtime-4ha7</link>
      <guid>https://dev.to/hamzezarenasiri/the-docker-compose-restart-policy-gotcha-that-cost-us-two-weeks-of-silent-downtime-4ha7</guid>
      <description>&lt;p&gt;We run a dozen+ services in plain &lt;code&gt;docker compose&lt;/code&gt; on a single box — no Swarm, no Kubernetes, just &lt;code&gt;docker-compose.prod.yml&lt;/code&gt; and &lt;code&gt;restart: always&lt;/code&gt; on everything. Or so I thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;A host reboot (unrelated maintenance) brought everything back up except three containers. Nobody noticed for two weeks, because the services that stayed down were dev-only instances and one internal monitoring stack — nothing customer-facing paged anyone.&lt;/p&gt;

&lt;p&gt;I found it by accident, running &lt;code&gt;docker ps -a&lt;/code&gt; for something unrelated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}\t{{.Status}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"exited"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three containers, all &lt;code&gt;Exited (0)&lt;/code&gt; from &lt;strong&gt;two weeks ago&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker compose down&lt;/code&gt; (run once, on purpose, weeks earlier, for an unrelated fix) stops and removes containers. When they got recreated on the next &lt;code&gt;up -d&lt;/code&gt;, a few of them had drifted from the rest of the fleet: no explicit &lt;code&gt;restart&lt;/code&gt; policy in their compose block, which means Docker defaults to &lt;code&gt;restart: "no"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;restart: "no"&lt;/code&gt; means exactly what it says — if the container exits for &lt;em&gt;any&lt;/em&gt; reason (a clean &lt;code&gt;down&lt;/code&gt;, a host reboot, an OOM kill), Docker will never bring it back. Not on daemon restart, not ever, until someone runs &lt;code&gt;up -d&lt;/code&gt; again by hand.&lt;/p&gt;

&lt;p&gt;The other ~90% of our services had &lt;code&gt;restart: always&lt;/code&gt; explicitly set, so a reboot was invisible for them. These three were added later by someone (me) who forgot the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner that would've caught it immediately
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;-aq&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"no"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Name}}'&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's a step in our monthly infra audit, not something we find by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second half of this problem: zero-downtime redeploys
&lt;/h2&gt;

&lt;p&gt;This same fleet has another sharp edge worth knowing about if you're running plain compose in production: &lt;code&gt;docker compose up -d &amp;lt;service&amp;gt;&lt;/code&gt; for a redeploy briefly &lt;strong&gt;stops the old container before the new one is healthy&lt;/strong&gt; — a few seconds of 502s behind Traefik/nginx.&lt;/p&gt;

&lt;p&gt;We ended up writing a small blue-green script instead of reaching for Swarm/K8s just for this one problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start a second container alongside the live one (&lt;code&gt;docker compose run -d --no-deps --name &amp;lt;container&amp;gt;_green &amp;lt;service&amp;gt;&lt;/code&gt; — has to be &lt;code&gt;run&lt;/code&gt;, not &lt;code&gt;up&lt;/code&gt;, since &lt;code&gt;up&lt;/code&gt; always reconciles to one container per service)&lt;/li&gt;
&lt;li&gt;poll its healthcheck until &lt;code&gt;healthy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;remove the old container, rename the new one into its place, restore &lt;code&gt;restart: always&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;on healthcheck timeout, remove the new container and leave the old one untouched — a safe no-op, not a rollback of something that already happened&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Script's public if useful: &lt;a href="https://github.com/Fanpino/deploy-bluegreen" rel="noopener noreferrer"&gt;https://github.com/Fanpino/deploy-bluegreen&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neither of these is a novel idea. But "explicit restart policy on every service" and "compose &lt;code&gt;up -d&lt;/code&gt; isn't zero-downtime" are the kind of things that are obvious in hindsight and invisible until they cost you two weeks.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
