<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Unattended-Upgrades on Lessons learned</title><link>https://eghuro.cz/tags/unattended-upgrades/</link><description>Recent content in Unattended-Upgrades on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sat, 26 Sep 2026 19:35:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/unattended-upgrades/index.xml" rel="self" type="application/rss+xml"/><item><title>unattended-upgrades on Debian that installs nothing: Ubuntu-style Allowed-Origins</title><link>https://eghuro.cz/posts/unattended-upgrades-debian-allowed-origins/</link><pubDate>Sat, 26 Sep 2026 19:35:00 +0200</pubDate><guid>https://eghuro.cz/posts/unattended-upgrades-debian-allowed-origins/</guid><description>&lt;p&gt;A Debian 13 server had automatic security updates. Ansible had installed unattended-upgrades, enabled it in &lt;code&gt;20auto-upgrades&lt;/code&gt;, and &lt;code&gt;apt-daily-upgrade.timer&lt;/code&gt; had run that morning. Then a routine check:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ apt list --upgradable 2&amp;gt;/dev/null | grep -c security&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;61&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing had failed, and nothing had warned. unattended-upgrades had never found anything it was allowed to install.&lt;/p&gt;&#10;&lt;p&gt;If your &lt;code&gt;50unattended-upgrades&lt;/code&gt; on Debian has &lt;code&gt;Allowed-Origins&lt;/code&gt; with &lt;code&gt;${distro_codename}-security&lt;/code&gt;, yours does the same.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-config"&gt;The config &lt;/h2&gt;&#10;&lt;p&gt;This is what the Ansible role wrote to &lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Unattended-Upgrade::Allowed-Origins {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;${distro_id}:${distro_codename}&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;${distro_id}:${distro_codename}-security&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;${distro_id}ESMApps:${distro_codename}-apps-security&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;${distro_id}ESM:${distro_codename}-infra-security&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The ESM lines give it away: Extended Security Maintenance is Ubuntu Pro. This is Ubuntu&amp;rsquo;s stock file, copied into a Debian role by hand or by an LLM. On Debian it expands to &amp;ldquo;Debian:trixie&amp;rdquo; and &amp;ldquo;Debian:trixie-security&amp;rdquo;, which looks right and matches nothing.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-it-matches-nothing"&gt;Why it matches nothing &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;Allowed-Origins&lt;/code&gt; is the legacy syntax. unattended-upgrades (2.12 on Debian 13) converts each entry to the newer pattern format in &lt;code&gt;get_allowed_origins_legacy()&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;allowed_origins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;o=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;,a=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;substitute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distro_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;substitute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distro_codename&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The part after the colon becomes &lt;code&gt;a=&lt;/code&gt;, and the matcher compares &lt;code&gt;a&lt;/code&gt; with the archive, which is the &lt;code&gt;Suite:&lt;/code&gt; field of the Release file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;suite&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;archive&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fnmatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Ubuntu, suite and codename are the same string:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# security.ubuntu.com/ubuntu/dists/noble-security/InRelease&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Suite: noble-security&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Codename: noble&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Debian, they are not:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# security.debian.org/debian-security/dists/trixie-security/InRelease&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Origin: Debian&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Label: Debian-Security&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Suite: stable-security&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Codename: trixie-security&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;apt-cache policy&lt;/code&gt; shows both, as &lt;code&gt;a=&lt;/code&gt; and &lt;code&gt;n=&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;release v=13,o=Debian,a=stable-security,n=trixie-security,l=Debian-Security,c=main,b=amd64&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So &lt;code&gt;a=trixie-security&lt;/code&gt; matches nothing, and neither does &lt;code&gt;a=trixie&lt;/code&gt;, because the main archive&amp;rsquo;s suite is &lt;code&gt;stable&lt;/code&gt;. Run through unattended-upgrades&amp;rsquo; own matcher against a trixie host&amp;rsquo;s package lists, the legacy entries come up empty, and Debian&amp;rsquo;s patterns each hit their archive:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;o=Debian,a=trixie&amp;#39; -&amp;gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;o=Debian,a=trixie-security&amp;#39; -&amp;gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;origin=Debian,codename=${distro_codename},label=Debian&amp;#39; -&amp;gt; [&amp;#39;stable/trixie&amp;#39;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;origin=Debian,codename=${distro_codename}-security,label=Debian-Security&amp;#39; -&amp;gt; [&amp;#39;stable-security/trixie-security&amp;#39;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With no allowed origin, there is nothing to upgrade, and that&amp;rsquo;s not an error. Each run logs &lt;code&gt;No packages found that can be upgraded unattended and no pending auto-removals&lt;/code&gt; and exits cleanly.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-fix"&gt;The fix &lt;/h2&gt;&#10;&lt;p&gt;Use &lt;code&gt;Origins-Pattern&lt;/code&gt; with &lt;code&gt;codename=&lt;/code&gt;, as Debian&amp;rsquo;s own file does:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Unattended-Upgrade::Origins-Pattern {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;origin=Debian,codename=${distro_codename},label=Debian&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;origin=Debian,codename=${distro_codename}-updates,label=Debian&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;origin=Debian,codename=${distro_codename},label=Debian-Security&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;origin=Debian,codename=${distro_codename}-security,label=Debian-Security&amp;#34;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Debian&amp;rsquo;s stock file has the &lt;code&gt;-updates&lt;/code&gt; line commented out. Keep it or drop it as you like.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;a=stable-security&lt;/code&gt; would match too, but don&amp;rsquo;t use it. On release day, &lt;code&gt;stable&lt;/code&gt; moves to the next Debian release, and a suite-based pattern would follow it. A codename stays with the release you installed.&lt;/p&gt;&#10;&lt;p&gt;Then check what unattended-upgrades actually picks:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo unattended-upgrade --dry-run -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;Allowed origins are:&lt;/code&gt; line shows the patterns after conversion. Below it, the security packages from &lt;code&gt;apt list --upgradable&lt;/code&gt; should show up as upgrade candidates. If the run ends with &amp;ldquo;No packages found&amp;rdquo; while &lt;code&gt;apt list --upgradable&lt;/code&gt; lists &lt;code&gt;stable-security&lt;/code&gt; packages, the origins are still wrong.&lt;/p&gt;&#10;&lt;p&gt;With &lt;code&gt;Automatic-Reboot &amp;quot;false&amp;quot;&lt;/code&gt;, a new kernel or libc still waits for you once updates flow again, so check for &lt;code&gt;/var/run/reboot-required&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;On Debian, &lt;code&gt;Allowed-Origins &amp;quot;${distro_id}:${distro_codename}-security&amp;quot;&lt;/code&gt; is a no-op that looks like a security policy, because it matches the suite (&lt;code&gt;stable-security&lt;/code&gt;), not the codename. Use &lt;code&gt;Origins-Pattern&lt;/code&gt; with &lt;code&gt;codename=&lt;/code&gt;, and confirm it with &lt;code&gt;--dry-run -d&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Run &lt;code&gt;grep -rn Allowed-Origins /etc/apt/apt.conf.d/&lt;/code&gt; on your Debian hosts, and run the same grep over your configuration management repos, where the copy is most likely to live.&lt;/p&gt;&#10;</description></item></channel></rss>