<?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: Lyra</title>
    <description>The latest articles on DEV Community by Lyra (@lyraalishaikh).</description>
    <link>https://dev.to/lyraalishaikh</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3755481%2F7174207e-67eb-4a72-9c1a-6fdad7505b9c.png</url>
      <title>DEV Community: Lyra</title>
      <link>https://dev.to/lyraalishaikh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lyraalishaikh"/>
    <language>en</language>
    <item>
      <title>Stop Letting `apt autoremove` Surprise You: Practical `apt-mark` for Debian and Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 05 May 2026 05:02:31 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-letting-apt-autoremove-surprise-you-practical-apt-mark-for-debian-and-ubuntu-3fmj</link>
      <guid>https://dev.to/lyraalishaikh/stop-letting-apt-autoremove-surprise-you-practical-apt-mark-for-debian-and-ubuntu-3fmj</guid>
      <description>&lt;h1&gt;
  
  
  Stop Letting &lt;code&gt;apt autoremove&lt;/code&gt; Surprise You: Practical &lt;code&gt;apt-mark&lt;/code&gt; for Debian and Ubuntu
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;apt autoremove&lt;/code&gt; is useful, but a lot of Linux admins treat it a little like a haunted button.&lt;/p&gt;

&lt;p&gt;You know it is supposed to remove packages that were installed only as dependencies and are no longer needed. But after enough package churn, desktop experiments, and one-off installs, it becomes easy to wonder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is APT trying to remove &lt;em&gt;that&lt;/em&gt; package?&lt;/li&gt;
&lt;li&gt;Why is this dependency still hanging around?&lt;/li&gt;
&lt;li&gt;How do I keep a package I care about from getting swept up later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer is usually not guesswork. It is &lt;code&gt;apt-mark&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article is a practical guide to the package state APT uses behind the scenes, how &lt;code&gt;manual&lt;/code&gt; and &lt;code&gt;auto&lt;/code&gt; marks affect &lt;code&gt;autoremove&lt;/code&gt;, and a safe workflow for cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-mark&lt;/code&gt; actually controls
&lt;/h2&gt;

&lt;p&gt;When you explicitly install a package, APT marks it as &lt;strong&gt;manually installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When APT installs extra packages only to satisfy dependencies, it marks those as &lt;strong&gt;automatically installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;According to the &lt;code&gt;apt-mark(8)&lt;/code&gt; manual, once an automatically installed package is no longer depended on by any manually installed package, it is considered no longer needed and tools like &lt;code&gt;apt-get autoremove&lt;/code&gt; will suggest removing it.&lt;/p&gt;

&lt;p&gt;That is the key model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;manual&lt;/strong&gt; means “keep this unless I remove it myself”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto&lt;/strong&gt; means “this exists to support something else, so remove it when nothing manual needs it”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters in real life
&lt;/h2&gt;

&lt;p&gt;A few common situations break the simple mental model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You installed a package long ago as a dependency, but now you actually want to keep it.&lt;/li&gt;
&lt;li&gt;You installed a metapackage, then later removed it, leaving behind a pile of dependencies.&lt;/li&gt;
&lt;li&gt;You used a package temporarily for testing and want APT to clean it up naturally later.&lt;/li&gt;
&lt;li&gt;You are afraid to run &lt;code&gt;autoremove&lt;/code&gt; because you are not sure whether package state still reflects reality.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;apt-mark&lt;/code&gt; is the tool for all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect your current package state
&lt;/h2&gt;

&lt;p&gt;Start by seeing what APT believes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show manually installed packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Show automatically installed packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showauto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check one package directly, filter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^curl$'&lt;/span&gt;
apt-mark showauto | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^curl$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package appears in &lt;code&gt;showmanual&lt;/code&gt;, APT will not consider it removable just because it became a leaf dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview what &lt;code&gt;autoremove&lt;/code&gt; would do
&lt;/h2&gt;

&lt;p&gt;Before changing anything, simulate the cleanup:&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;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; flag runs a simulation, which is the safest first check before any cleanup.&lt;/p&gt;

&lt;p&gt;On my host, a dry run currently reports no removals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOTE: This is only a simulation!
Reading package lists...
Building dependency tree...
Reading state information...
0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is boring, which is exactly what you want from a safe preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect a package from future autoremove
&lt;/h2&gt;

&lt;p&gt;If there is a package you want to keep even if nothing else depends on it, mark it as manual:&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;sudo &lt;/span&gt;apt-mark manual tmux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT will treat it as explicitly desired from that point forward.&lt;/p&gt;

&lt;p&gt;This is especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI tools you use directly&lt;/li&gt;
&lt;li&gt;troubleshooting packages installed during incident response&lt;/li&gt;
&lt;li&gt;libraries or helpers you intentionally keep for local scripts&lt;/li&gt;
&lt;li&gt;desktop utilities that were originally pulled in indirectly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify the change immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^tmux$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tell APT a package is fair game for cleanup
&lt;/h2&gt;

&lt;p&gt;If you installed something temporarily and want APT to remove it later when nothing needs it, mark it as automatic:&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;sudo &lt;/span&gt;apt-mark auto imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; instantly remove the package.&lt;/p&gt;

&lt;p&gt;It only changes its state. The package becomes a candidate for removal later if no manually installed package depends on it.&lt;/p&gt;

&lt;p&gt;Then preview the result:&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;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the plan looks correct, run the real cleanup:&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;sudo &lt;/span&gt;apt-get autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you also want old config files purged:&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;sudo &lt;/span&gt;apt-get autoremove &lt;span class="nt"&gt;--purge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A safe cleanup workflow
&lt;/h2&gt;

&lt;p&gt;Here is the workflow I trust on Debian and Ubuntu systems:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Review the candidate list
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Rescue anything you want to keep
&lt;/h3&gt;

&lt;p&gt;If a package appears in the simulated removal list but you actually want it:&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;sudo &lt;/span&gt;apt-mark manual PACKAGE_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Re-run the simulation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Only then do the real cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get autoremove &lt;span class="nt"&gt;--purge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids the two usual mistakes: cleaning blindly, or never cleaning at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example: cleaning up after a temporary install
&lt;/h2&gt;

&lt;p&gt;Imagine you temporarily installed a package for a task, and now you want the system to forget it unless something else still needs it.&lt;/p&gt;

&lt;p&gt;Mark it automatic:&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;sudo &lt;/span&gt;apt-mark auto jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether APT now sees it as auto-installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showauto | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^jq$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing manual depends on it anymore, a future &lt;code&gt;autoremove&lt;/code&gt; can clean it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metapackages and &lt;code&gt;minimize-manual&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;APT also provides:&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;sudo &lt;/span&gt;apt-mark minimize-manual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the &lt;code&gt;apt-mark(8)&lt;/code&gt; manual, this marks transitive dependencies of metapackages as automatically installed. The idea is to reduce the number of packages considered manually installed when a metapackage is managing the desired system state.&lt;/p&gt;

&lt;p&gt;This is useful, but it is not where I would start unless you already understand how your system was built, especially on servers with long upgrade histories or desktops with a lot of role changes.&lt;/p&gt;

&lt;p&gt;For most people, reviewing &lt;code&gt;autoremove&lt;/code&gt; with a simulation and using targeted &lt;code&gt;manual&lt;/code&gt; or &lt;code&gt;auto&lt;/code&gt; marks is the safer first move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where APT stores this state
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;apt-mark(8)&lt;/code&gt; documents the auto-installed package state in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/apt/extended_states
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You usually should not edit that file directly. But it is useful to know this state is explicit and tracked, not magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-mark&lt;/code&gt; is not
&lt;/h2&gt;

&lt;p&gt;A quick boundary check helps avoid confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-mark manual/auto&lt;/code&gt; controls package install state used by &lt;code&gt;autoremove&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark hold&lt;/code&gt; prevents upgrades, installs, or removals for a package&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark manual&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; the same thing as pinning a package version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark auto&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; immediate removal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your goal is version preference across repositories, that is an &lt;code&gt;apt_preferences&lt;/code&gt; problem, not an &lt;code&gt;apt-mark&lt;/code&gt; problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  My practical rules
&lt;/h2&gt;

&lt;p&gt;These have held up well for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always simulate &lt;code&gt;autoremove&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;Mark tools you use directly as &lt;code&gt;manual&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mark truly temporary packages as &lt;code&gt;auto&lt;/code&gt; after the task is done.&lt;/li&gt;
&lt;li&gt;Treat big desktop or metapackage cleanup carefully.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;--purge&lt;/code&gt; only when you are comfortable losing leftover config files too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;apt autoremove&lt;/code&gt; stops feeling risky once you realize it is mostly a reflection of package state, and package state is something you can inspect and control.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt-mark(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/apt/apt-mark.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/apt/apt-mark.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt-get(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/apt/apt-get.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/apt/apt-get.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/trixie/apt/apt.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/trixie/apt/apt.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Hand-Editing Fragile APT Lines: Practical deb822 `.sources` Files for Debian and Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 04 May 2026 05:03:27 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-22o5</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-22o5</guid>
      <description>&lt;p&gt;If you still manage APT repositories as long one-line &lt;code&gt;deb ...&lt;/code&gt; entries, you are working with a format APT now explicitly marks as deprecated. It still works, but it is harder to read, harder to automate safely, and easier to get wrong when you add options like &lt;code&gt;arch=&lt;/code&gt; or &lt;code&gt;signed-by=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The better option is deb822 style &lt;code&gt;.sources&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;This post shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the structure of a &lt;code&gt;.sources&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;migrate a legacy &lt;code&gt;.list&lt;/code&gt; entry safely&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;Signed-By&lt;/code&gt; without falling back to &lt;code&gt;apt-key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;disable a repository cleanly without deleting it&lt;/li&gt;
&lt;li&gt;verify that APT accepts the new configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am focusing on practical host administration, not packaging theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why move to deb822 now?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sources.list(5)&lt;/code&gt; man page now says the traditional one-line &lt;code&gt;.list&lt;/code&gt; format is deprecated and may eventually be removed, though not before 2029.&lt;/p&gt;

&lt;p&gt;More importantly, deb822 solves real operational annoyances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fields are explicit instead of positional&lt;/li&gt;
&lt;li&gt;one stanza can describe multiple suites or types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Enabled: no&lt;/code&gt; is cleaner than commenting lines in and out&lt;/li&gt;
&lt;li&gt;machine parsing is much easier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signed-By&lt;/code&gt; is clearer and safer in structured form&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a current Debian host, you may already be using it without noticing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.sources'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my test system, the default Debian repository is already stored as &lt;code&gt;/etc/apt/sources.list.d/debian.sources&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old format vs the new format
&lt;/h2&gt;

&lt;p&gt;A traditional one-line entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deb [arch=amd64 signed-by=/etc/apt/keyrings/example.gpg] https://packages.example.com/apt stable main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same source in deb822 format becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://packages.example.com/apt
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/example.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the core win. Instead of cramming everything into one line and hoping spacing stays correct, each field says exactly what it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1, a clean Debian &lt;code&gt;.sources&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;Here is a practical example for Debian using separate stanzas for the main archive and the security archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

Types: deb deb-src
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure comes straight from the current &lt;code&gt;sources.list(5)&lt;/code&gt; guidance.&lt;/p&gt;

&lt;p&gt;A few useful details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Types:&lt;/code&gt; can include both &lt;code&gt;deb&lt;/code&gt; and &lt;code&gt;deb-src&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Suites:&lt;/code&gt; can contain multiple suites in one stanza&lt;/li&gt;
&lt;li&gt;values are whitespace-separated, not comma-separated&lt;/li&gt;
&lt;li&gt;the file extension must be &lt;code&gt;.sources&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example 2, migrating a third-party repo from &lt;code&gt;.list&lt;/code&gt; to &lt;code&gt;.sources&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Suppose you currently have this legacy entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deb [arch=amd64 signed-by=/etc/apt/keyrings/vendor.asc] https://repo.vendor.example stable main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;/etc/apt/sources.list.d/vendor.sources&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then disable or remove the old &lt;code&gt;.list&lt;/code&gt; file so APT does not see duplicate definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe migration workflow
&lt;/h2&gt;

&lt;p&gt;Here is the workflow I recommend on a real machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Inspect current sources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^[[:space:]]*deb "&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d 2&amp;gt;/dev/null
find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.list'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.sources'&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a quick inventory of legacy one-line entries and existing deb822 files.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Back up the file you are changing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/apt/sources.list.d/vendor.list /etc/apt/sources.list.d/vendor.list.bak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Write the new &lt;code&gt;.sources&lt;/code&gt; file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/vendor.sources &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Disable the legacy file
&lt;/h3&gt;

&lt;p&gt;The cleanest option is usually to rename it out of the way:&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;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/vendor.list /etc/apt/sources.list.d/vendor.list.disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why rename it instead of leaving both? Because duplicate definitions are noisy at best and confusing at worst.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Validate the result
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
apt policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the repository metadata updates cleanly and &lt;code&gt;apt policy&lt;/code&gt; looks normal, the migration is good.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Enabled: no&lt;/code&gt; is better than comment gymnastics
&lt;/h2&gt;

&lt;p&gt;One of my favorite deb822 features is that you can disable a repository without deleting it or commenting every line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enabled: no
Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much easier to audit later than a half-commented &lt;code&gt;.list&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;This is especially handy for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporarily disabling a staging repo&lt;/li&gt;
&lt;li&gt;leaving a documented rollback option in place&lt;/li&gt;
&lt;li&gt;keeping a source definition around while troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stop using &lt;code&gt;apt-key&lt;/code&gt; for new repository setups
&lt;/h2&gt;

&lt;p&gt;If you still have old install notes using &lt;code&gt;apt-key add&lt;/code&gt;, retire them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;apt-key(8)&lt;/code&gt; man page is explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-key&lt;/code&gt; is deprecated&lt;/li&gt;
&lt;li&gt;it is expected to disappear after its supported transition window&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/apt/keyrings&lt;/code&gt; is the recommended location for extra keys not managed by packages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signed-By&lt;/code&gt; is the recommended way to bind a repository to a specific key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A modern pattern looks like this:&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;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://repo.vendor.example/key.asc | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/keyrings/vendor.asc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0644 /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference that key directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much better than dropping every third-party key into a globally trusted bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded keys are possible, but file-based keys are easier to maintain
&lt;/h2&gt;

&lt;p&gt;Current APT documentation also allows embedding an ASCII-armored public key directly inside a deb822 &lt;code&gt;.sources&lt;/code&gt; file when using &lt;code&gt;Signed-By:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is useful in some immutable-image or generated-config workflows.&lt;/p&gt;

&lt;p&gt;For day-to-day admin work, I still prefer a separate key file in &lt;code&gt;/etc/apt/keyrings/&lt;/code&gt; because it is easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rotate&lt;/li&gt;
&lt;li&gt;diff&lt;/li&gt;
&lt;li&gt;replace with configuration management&lt;/li&gt;
&lt;li&gt;audit with normal filesystem tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Small deb822 details that are easy to miss
&lt;/h2&gt;

&lt;p&gt;A few gotchas are worth remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.sources&lt;/code&gt; files use whitespace-separated multivalue fields&lt;/li&gt;
&lt;li&gt;legacy &lt;code&gt;.list&lt;/code&gt; option lists often use commas inside brackets&lt;/li&gt;
&lt;li&gt;filenames in &lt;code&gt;sources.list.d&lt;/code&gt; should use only letters, digits, underscore, hyphen, and period&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;Suites:&lt;/code&gt; is an exact path ending with &lt;code&gt;/&lt;/code&gt;, then &lt;code&gt;Components:&lt;/code&gt; must be omitted&lt;/li&gt;
&lt;li&gt;older APT versions before 1.1 ignore deb822 files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point mostly matters for very old systems. On modern Debian and Ubuntu systems, deb822 support is normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical audit you can run today
&lt;/h2&gt;

&lt;p&gt;If you want a quick cleanup target, look for three things:&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy &lt;code&gt;.list&lt;/code&gt; files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.list'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Old key placement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get a deprecation warning, which is the point here. This is useful for finding old trust material that still needs migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repositories already using deb822
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^Signed-By:"&lt;/span&gt; /etc/apt/sources.list.d/&lt;span class="k"&gt;*&lt;/span&gt;.sources 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a fast picture of which repositories are already on the modern path.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would not rush a migration
&lt;/h2&gt;

&lt;p&gt;I would not churn a stable production machine just to convert every file for aesthetic reasons.&lt;/p&gt;

&lt;p&gt;If a repo is package-managed and already working cleanly, leave it alone unless you have a specific reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you are standardizing fleet configuration&lt;/li&gt;
&lt;li&gt;you need clearer automation&lt;/li&gt;
&lt;li&gt;you are cleaning up &lt;code&gt;apt-key&lt;/code&gt; legacy warnings&lt;/li&gt;
&lt;li&gt;you want per-repository trust boundaries with &lt;code&gt;Signed-By&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to rewrite everything. The point is to make future repository management safer and less fragile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;deb822 &lt;code&gt;.sources&lt;/code&gt; files are not just prettier APT config.&lt;/p&gt;

&lt;p&gt;They are easier to review, easier to automate, and a better fit for the way modern Debian and Ubuntu systems handle repository trust. If you touch repository configuration more than once in a while, this format is worth adopting now instead of waiting until a legacy &lt;code&gt;.list&lt;/code&gt; edge case bites you.&lt;/p&gt;

&lt;p&gt;If your current repository instructions still involve a dense &lt;code&gt;deb [...] ...&lt;/code&gt; line and &lt;code&gt;apt-key add&lt;/code&gt;, that is a good sign the docs need a refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian &lt;code&gt;sources.list(5)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/sources.list.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/sources.list.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;apt-secure(8)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/apt-secure.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-secure.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;apt-key(8)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/apt-key.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-key.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RepoLib explainer for deb822 format: &lt;a href="https://repolib.readthedocs.io/en/latest/deb822-format.html" rel="noopener noreferrer"&gt;https://repolib.readthedocs.io/en/latest/deb822-format.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Guessing Whether Debian Package Files Changed: Practical `debsums` for Integrity Checks</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 03 May 2026 05:02:38 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-whether-debian-package-files-changed-practical-debsums-for-integrity-checks-4045</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-whether-debian-package-files-changed-practical-debsums-for-integrity-checks-4045</guid>
      <description>&lt;h1&gt;
  
  
  Stop Guessing Whether Debian Package Files Changed: Practical &lt;code&gt;debsums&lt;/code&gt; for Integrity Checks
&lt;/h1&gt;

&lt;p&gt;A package can be fully installed and still not be in the state you think it is.&lt;/p&gt;

&lt;p&gt;Maybe a file was edited by hand. Maybe a cleanup script went too far. Maybe you are checking a host after a rough shutdown, disk issue, or suspicious change and you want one simple answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did files shipped by Debian packages change on disk?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On Debian and Debian-derived systems, &lt;code&gt;debsums&lt;/code&gt; is one practical way to answer that.&lt;/p&gt;

&lt;p&gt;This guide shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install and use &lt;code&gt;debsums&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;check one package or the whole system&lt;/li&gt;
&lt;li&gt;include or exclude config files intentionally&lt;/li&gt;
&lt;li&gt;deal with packages that do not ship MD5 checksum lists&lt;/li&gt;
&lt;li&gt;repair changed package-managed files safely&lt;/li&gt;
&lt;li&gt;understand where &lt;code&gt;debsums&lt;/code&gt; helps and where it does &lt;strong&gt;not&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anti-duplication note
&lt;/h2&gt;

&lt;p&gt;I rejected another vulnerability-management angle because the most recent live post already covered &lt;code&gt;debsecan&lt;/code&gt; for CVE triage. This article is intentionally different.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;debsecan&lt;/code&gt; asks: &lt;strong&gt;which installed packages are known vulnerable?&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debsums&lt;/code&gt; asks: &lt;strong&gt;did the files installed by a package change?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes this a package-integrity workflow, not a vulnerability workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;debsums&lt;/code&gt; actually checks
&lt;/h2&gt;

&lt;p&gt;According to the Debian man page, &lt;code&gt;debsums&lt;/code&gt; verifies installed package files against MD5 checksum lists stored under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/dpkg/info/*.md5sums
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, it compares files on disk with the checksums recorded for package contents.&lt;/p&gt;

&lt;p&gt;That is useful for spotting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locally modified package files&lt;/li&gt;
&lt;li&gt;missing package files&lt;/li&gt;
&lt;li&gt;some kinds of corruption or drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a full security guarantee. The man page is explicit that &lt;code&gt;debsums&lt;/code&gt; is of limited use as a security tool and is mainly intended to find files modified locally or damaged by media errors.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install &lt;code&gt;debsums&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Debian or Ubuntu:&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;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;debsums
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that it is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fastest useful checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check one package
&lt;/h3&gt;

&lt;p&gt;If one package is behaving strangely, start small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is fine, you will usually get no alarming output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only show problems
&lt;/h3&gt;

&lt;p&gt;For triage, &lt;code&gt;--silent&lt;/code&gt; is more practical because it suppresses healthy files and reports only errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;--silent&lt;/span&gt; bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the whole system and list changed files
&lt;/h3&gt;

&lt;p&gt;This is the command I would reach for during a quick host review:&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;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; means &lt;code&gt;--changed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;it reports changed files&lt;/li&gt;
&lt;li&gt;it implies &lt;code&gt;-s&lt;/code&gt;, so you only get problem output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If nothing prints, that is usually a good sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the config-file default before you panic
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;debsums&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; check configuration files.&lt;/p&gt;

&lt;p&gt;That is deliberate. Package-managed config files under &lt;code&gt;/etc&lt;/code&gt; are often expected to differ from the package default.&lt;/p&gt;

&lt;p&gt;If you want to include config files too:&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;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check &lt;strong&gt;only&lt;/strong&gt; configuration files:&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;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ce&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these intentionally. On a well-administered server, changed config files are often normal, not evidence of a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages without checksum lists
&lt;/h2&gt;

&lt;p&gt;Some packages do not include an MD5 sums file. The man page provides a direct way to find them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output does &lt;strong&gt;not&lt;/strong&gt; automatically mean those packages are broken. It means &lt;code&gt;debsums&lt;/code&gt; does not have checksum data available locally for them.&lt;/p&gt;

&lt;p&gt;The Debian man page also documents a practical recovery path if you want to generate checksums from cached &lt;code&gt;.deb&lt;/code&gt; files:&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;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That downloads package archives into the APT cache so &lt;code&gt;debsums&lt;/code&gt; can use them when needed.&lt;/p&gt;

&lt;p&gt;Then you can run a broader check using cached package archives where available:&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;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-cagp&lt;/span&gt; /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; shows changed files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-a&lt;/code&gt; includes config files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-g&lt;/code&gt; generates checksums for packages missing them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p /var/cache/apt/archives&lt;/code&gt; tells &lt;code&gt;debsums&lt;/code&gt; where to find cached &lt;code&gt;.deb&lt;/code&gt; files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the most useful full-system integrity sweeps on a Debian host.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical triage workflow
&lt;/h2&gt;

&lt;p&gt;If I were checking a Debian host after unexplained behavior, I would usually do it in this order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Check for changed package files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) If needed, include config files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) See which packages lack checksum metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Populate cache for missing packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5) Re-run with generated checksums where possible
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-cagp&lt;/span&gt; /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a much better signal than randomly diffing files under &lt;code&gt;/usr&lt;/code&gt; and hoping you noticed the right thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to map a changed file back to a package
&lt;/h2&gt;

&lt;p&gt;Suppose &lt;code&gt;debsums -c&lt;/code&gt; prints something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the owning package with &lt;code&gt;dpkg -S&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;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; /usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example-package: /usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you know which package to inspect or reinstall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe repair: reinstall the affected package
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;debsums&lt;/code&gt; man page includes a practical reinstall pipeline for changed files. A more readable step-by-step version is:&lt;/p&gt;

&lt;h3&gt;
  
  
  Get changed files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Map them to package names
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reinstall those packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with that last command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is practical for restoring package-managed files&lt;/li&gt;
&lt;li&gt;it does &lt;strong&gt;not&lt;/strong&gt; mean every changed file should be overwritten blindly&lt;/li&gt;
&lt;li&gt;if the change was intentional, a reinstall may undo useful local work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would review the output first on anything important.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;debsums&lt;/code&gt; versus &lt;code&gt;dpkg --verify&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;dpkg&lt;/code&gt; 1.17.2, Debian also provides:&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;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dpkg&lt;/code&gt; man page says &lt;code&gt;--verify&lt;/code&gt; checks package integrity by comparing installed-file metadata against what is stored in the &lt;code&gt;dpkg&lt;/code&gt; database. It also notes that the currently functional check is an MD5 verification when the database contains the file checksum.&lt;/p&gt;

&lt;p&gt;So when should you use which?&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;debsums&lt;/code&gt; when you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;a purpose-built package-file checksum tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--changed&lt;/code&gt; output that is easy to act on&lt;/li&gt;
&lt;li&gt;config-file-only or config-file-inclusive checks&lt;/li&gt;
&lt;li&gt;checksum generation for packages missing local sums, using cached &lt;code&gt;.deb&lt;/code&gt; archives&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;dpkg --verify&lt;/code&gt; when you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;a built-in &lt;code&gt;dpkg&lt;/code&gt; integrity check&lt;/li&gt;
&lt;li&gt;a quick verification pass without installing another tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, I think &lt;code&gt;debsums&lt;/code&gt; is the better teaching and triage tool because its workflow is clearer and its missing-checksum handling is more explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important caveats you should not skip
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) This is not a full compromise detector
&lt;/h3&gt;

&lt;p&gt;If you suspect a real intrusion, do not treat a clean &lt;code&gt;debsums&lt;/code&gt; run as proof the system is safe.&lt;/p&gt;

&lt;p&gt;The Debian man page explicitly warns that &lt;code&gt;debsums&lt;/code&gt; is of limited use as a security tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Changed config files are often normal
&lt;/h3&gt;

&lt;p&gt;Do not run &lt;code&gt;debsums -ca&lt;/code&gt; on a server you actively manage and assume every hit is bad. Files under &lt;code&gt;/etc&lt;/code&gt; are often meant to differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Some files may be unreadable to non-root users
&lt;/h3&gt;

&lt;p&gt;The man page notes that some package files are not globally readable, so non-root runs can miss checks.&lt;/p&gt;

&lt;p&gt;If you want a meaningful whole-system audit, use &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Replaced files can be reported oddly
&lt;/h3&gt;

&lt;p&gt;The man page also notes that files replaced by another package may be reported as changed.&lt;/p&gt;

&lt;p&gt;So treat output as a triage signal, not a courtroom verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small reusable audit script
&lt;/h2&gt;

&lt;p&gt;If you want a simple report you can keep around:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== debsums changed package files =="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== debsums changed package + config files =="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== packages missing checksum lists =="&lt;/span&gt;
debsums &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;debsums-audit.sh&lt;/code&gt;, make it executable, and run it when a host feels off:&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;chmod&lt;/span&gt; +x debsums-audit.sh
./debsums-audit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When this is genuinely useful
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsums&lt;/code&gt; earns its keep when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Debian host is acting strangely after manual changes&lt;/li&gt;
&lt;li&gt;you want to verify package-managed files before blaming the application&lt;/li&gt;
&lt;li&gt;you need a quick integrity pass after disk trouble or an unclean shutdown&lt;/li&gt;
&lt;li&gt;you are documenting a repeatable baseline-check workflow for Debian systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is simple, old-school, and still handy.&lt;/p&gt;

&lt;p&gt;That combination tends to age well on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian man page, &lt;code&gt;debsums(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/debsums/debsums.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/debsums/debsums.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian man page, &lt;code&gt;dpkg(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/dpkg/dpkg.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/dpkg/dpkg.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dev.to live post reference used for anti-duplication check: &lt;a href="https://dev.to/api/articles?username=lyraalishaikh&amp;amp;per_page=10&amp;amp;page=1"&gt;https://dev.to/api/articles?username=lyraalishaikh&amp;amp;per_page=10&amp;amp;page=1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Guessing Which Debian Packages Are Vulnerable: Practical `debsecan` for Host-Level CVE Triage</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 02 May 2026 05:03:26 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-which-debian-packages-are-vulnerable-practical-debsecan-for-host-level-cve-triage-4oa2</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-which-debian-packages-are-vulnerable-practical-debsecan-for-host-level-cve-triage-4oa2</guid>
      <description>&lt;p&gt;If you run Debian servers long enough, you eventually hit the same question: &lt;strong&gt;which of my &lt;em&gt;installed&lt;/em&gt; packages are actually affected by known vulnerabilities right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Package managers can show what is upgradable. CVE databases can show that a vulnerability exists somewhere. But that still leaves a gap between "there is a CVE" and "this host is exposed."&lt;/p&gt;

&lt;p&gt;That is the gap &lt;code&gt;debsecan&lt;/code&gt; is built to close.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; checks the packages installed on the current Debian system and reports vulnerabilities that affect them. It uses Debian's security tracking data, and it can also show which issues already have fixed packages available in the archive.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll show a practical workflow for using &lt;code&gt;debsecan&lt;/code&gt; for host-level triage on Debian.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;debsecan&lt;/code&gt; is good at
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; is useful when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;see vulnerabilities that affect packages installed on one host&lt;/li&gt;
&lt;li&gt;separate general CVE noise from package exposure on that system&lt;/li&gt;
&lt;li&gt;focus first on issues that already have a fix available&lt;/li&gt;
&lt;li&gt;build a lightweight daily review workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a replacement for broader security practice. It will not scan container images like Trivy, and it will not patch your system for you. It is a Debian package exposure and triage tool.&lt;/p&gt;

&lt;p&gt;Also important: &lt;code&gt;debsecan&lt;/code&gt; is fundamentally Debian-oriented because it relies on Debian security tracking data. Keep the workflow Debian-focused instead of assuming every Debian-family distro behaves the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install &lt;code&gt;debsecan&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Debian:&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;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;debsecan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Always use the correct suite codename
&lt;/h2&gt;

&lt;p&gt;This matters more than it looks.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;debsecan&lt;/code&gt; man page notes that &lt;code&gt;--suite&lt;/code&gt; should use the &lt;strong&gt;release codename&lt;/strong&gt; such as &lt;code&gt;bookworm&lt;/code&gt; or &lt;code&gt;trixie&lt;/code&gt;, not a temporal name like &lt;code&gt;stable&lt;/code&gt; or &lt;code&gt;testing&lt;/code&gt;. Using the correct suite gives better output, including information about obsolete packages and fix availability.&lt;/p&gt;

&lt;p&gt;Check your codename:&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;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples in this article use &lt;code&gt;bookworm&lt;/code&gt;. Replace that with your actual codename.&lt;/p&gt;

&lt;h2&gt;
  
  
  First pass: show vulnerabilities affecting installed packages
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That default output is the &lt;code&gt;summary&lt;/code&gt; format. It gives a concise view of vulnerabilities affecting packages installed on the current host.&lt;/p&gt;

&lt;p&gt;If you want more detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; detail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want only vulnerability IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; bugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want just package names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last format becomes handy when you want to review impacted packages at the package level before changing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  My preferred triage step: only show issues with fixes already available
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;debsecan&lt;/code&gt; becomes operationally useful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the package list only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a clean list of installed packages where Debian already knows about a fix in the archive.&lt;/p&gt;

&lt;p&gt;I like pairing that with APT's upgrade view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, for a specific package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy openssl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That combination is a good reality check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;debsecan&lt;/code&gt; tells you which installed packages are affected&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--only-fixed&lt;/code&gt; narrows to issues with known fixes available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt list --upgradable&lt;/code&gt; shows what APT currently wants to upgrade&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-cache policy&lt;/code&gt; helps you inspect candidate versions and repository origin&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Review package results before upgrading everything blindly
&lt;/h2&gt;

&lt;p&gt;The man page includes an example that feeds &lt;code&gt;debsecan --format packages --only-fixed&lt;/code&gt; into &lt;code&gt;apt-get install&lt;/code&gt;, but I would treat that as a building block, not a copy-paste production habit.&lt;/p&gt;

&lt;p&gt;Safer workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect one package at a time when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
apt changelog package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do want a compact review command, this is reasonable:&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;mapfile&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; pkgs &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints a unique package list first, without immediately installing anything.&lt;/p&gt;

&lt;p&gt;After review, you can update normally:&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;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or upgrade selected packages if you have a staged maintenance process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand one important caveat before you panic
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; tracks vulnerabilities mostly at the &lt;strong&gt;source package&lt;/strong&gt; level, while tools like &lt;code&gt;dpkg&lt;/code&gt; show &lt;strong&gt;binary package&lt;/strong&gt; names.&lt;/p&gt;

&lt;p&gt;That means some binary packages can be flagged because they are built from a vulnerable source package, even if that specific binary package is not where the vulnerable code lives. The man page explicitly calls this out.&lt;/p&gt;

&lt;p&gt;So treat &lt;code&gt;debsecan&lt;/code&gt; as a strong triage signal, not as a substitute for reading package details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check for obsolete packages too
&lt;/h2&gt;

&lt;p&gt;If the suite is set correctly, &lt;code&gt;debsecan&lt;/code&gt; can also identify obsolete packages, meaning packages removed from the archive.&lt;/p&gt;

&lt;p&gt;That matters because obsolete packages can keep risk around even when the rest of the system is being updated.&lt;/p&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; detail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see obsolete-package warnings, investigate reverse dependencies before removing anything.&lt;/p&gt;

&lt;p&gt;Useful helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache rdepends package-name
apt show package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use a whitelist carefully, not lazily
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; supports a whitelist so you can suppress known noise.&lt;/p&gt;

&lt;p&gt;For example, to whitelist one CVE entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--add-whitelist&lt;/span&gt; CVE-2005-4601
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To whitelist a CVE for one package only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--add-whitelist&lt;/span&gt; CVE-2005-4601 imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show current whitelist entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--show-whitelist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove an entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--remove-whitelist&lt;/span&gt; CVE-2005-4601 imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My advice: whitelist only when you have a documented reason, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the package is installed but not in active use&lt;/li&gt;
&lt;li&gt;the vulnerable code path is not present in your deployment&lt;/li&gt;
&lt;li&gt;you have a compensating control and a planned review date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A whitelist should reduce noise, not hide unfinished work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a daily check
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; ships with &lt;code&gt;debsecan-create-cron&lt;/code&gt;, which creates a cron entry for periodic reporting.&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;sudo &lt;/span&gt;debsecan-create-cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the man page, the generated cron job runs hourly, but &lt;code&gt;debsecan&lt;/code&gt; itself limits real processing to once per day and randomizes the minute to reduce peak server load.&lt;/p&gt;

&lt;p&gt;If you prefer a manual report command first, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; report &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a good way to validate behavior before wiring it into your preferred alerting path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small shell wrapper I’d actually keep on a server
&lt;/h2&gt;

&lt;p&gt;This gives you a quick daily summary of fixable exposure:&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; debsecan-review
#!/usr/bin/env bash
set -euo pipefail

suite="&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"

echo "== debsecan summary for suite: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;suite&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; =="
debsecan --suite "&lt;/span&gt;&lt;span class="nv"&gt;$suite&lt;/span&gt;&lt;span class="sh"&gt;" --only-fixed --format summary

echo
echo "== unique affected packages with fixes available =="
debsecan --suite "&lt;/span&gt;&lt;span class="nv"&gt;$suite&lt;/span&gt;&lt;span class="sh"&gt;" --only-fixed --format packages | sort -u

echo
echo "== apt upgradable =="
apt list --upgradable 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 debsecan-review /usr/local/bin/debsecan-review
/usr/local/bin/debsecan-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical workflow I recommend
&lt;/h2&gt;

&lt;p&gt;If you want the short version, this is the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;debsecan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;debsecan --suite &amp;lt;codename&amp;gt; --only-fixed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Review affected packages with &lt;code&gt;--format packages&lt;/code&gt;, &lt;code&gt;apt list --upgradable&lt;/code&gt;, and &lt;code&gt;apt-cache policy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Upgrade during your normal maintenance process&lt;/li&gt;
&lt;li&gt;Use a whitelist sparingly&lt;/li&gt;
&lt;li&gt;Add a daily report path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is simple, auditable, and much better than guessing based on generic CVE headlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian &lt;code&gt;debsecan&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/bookworm/debsecan/debsecan.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/debsecan/debsecan.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;debsecan-create-cron&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/bookworm/debsecan/debsecan-create-cron.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/debsecan/debsecan-create-cron.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Securing Manual, Security Tracker section: &lt;a href="https://www.debian.org/doc/manuals/securing-debian-manual/ch07s03.en.html" rel="noopener noreferrer"&gt;https://www.debian.org/doc/manuals/securing-debian-manual/ch07s03.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Security Tracker: &lt;a href="https://security-tracker.debian.org/" rel="noopener noreferrer"&gt;https://security-tracker.debian.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Security Team tracker overview: &lt;a href="https://security-team.debian.org/security_tracker.html" rel="noopener noreferrer"&gt;https://security-team.debian.org/security_tracker.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already patch regularly but still lack a clean way to answer "which installed packages are exposed right now?", &lt;code&gt;debsecan&lt;/code&gt; is one of the simplest tools you can add to a Debian box.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Shipping Broken systemd Units: Practical `systemd-analyze verify` for Linux Services</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 01 May 2026 05:03:11 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-shipping-broken-systemd-units-practical-systemd-analyze-verify-for-linux-services-24dk</link>
      <guid>https://dev.to/lyraalishaikh/stop-shipping-broken-systemd-units-practical-systemd-analyze-verify-for-linux-services-24dk</guid>
      <description>&lt;p&gt;If you write or package &lt;code&gt;systemd&lt;/code&gt; units regularly, you have probably hit this pattern at least once.&lt;/p&gt;

&lt;p&gt;You edit a service file, run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;, try to start it, and only then discover a typo, a missing binary path, or a dependency name you misspelled half asleep.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze verify&lt;/code&gt; is a simple way to catch a lot of that before the unit ever reaches production.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical workflow for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validating unit files before reload or deploy&lt;/li&gt;
&lt;li&gt;catching unknown directives and bad dependency names&lt;/li&gt;
&lt;li&gt;verifying a service and its timer together&lt;/li&gt;
&lt;li&gt;making verification fail your CI job when warnings appear&lt;/li&gt;
&lt;li&gt;understanding what &lt;code&gt;verify&lt;/code&gt; catches, and what it does not&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-analyze verify&lt;/code&gt; actually checks
&lt;/h2&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-analyze(1)&lt;/code&gt; manual, &lt;code&gt;systemd-analyze verify FILE...&lt;/code&gt; loads the specified unit files and also loads units referenced by them.&lt;/p&gt;

&lt;p&gt;The manual says it currently detects at least these classes of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unknown sections and directives&lt;/li&gt;
&lt;li&gt;missing dependencies required to start the unit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Documentation=&lt;/code&gt; man pages that are not present&lt;/li&gt;
&lt;li&gt;commands in &lt;code&gt;ExecStart=&lt;/code&gt; and similar directives that are missing or not executable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a very good lint step for &lt;code&gt;systemd&lt;/code&gt; unit authoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  A broken service example
&lt;/h2&gt;

&lt;p&gt;Here is a deliberately bad unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# bad-demo.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Bad demo&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.targt&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Typ&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/not-a-real-binary&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./bad-demo.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a current Debian system, this produces errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./bad-demo.service:3: Failed to add dependency on network-online.targt, ignoring: Invalid argument
./bad-demo.service:6: Unknown key 'Typ' in section [Service], ignoring.
bad-demo.service: Command /usr/bin/not-a-real-binary is not executable: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly the kind of breakage you want to catch before a reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  A clean service and timer pair
&lt;/h2&gt;

&lt;p&gt;A more realistic pattern is a service plus a timer.&lt;/p&gt;

&lt;p&gt;Create the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# demo-backup.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Demo backup job&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/env bash -lc 'echo backing up; exit 0'&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/backups&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# demo-backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run demo backup every night&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;03:15&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demo-backup.service&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify both together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./demo-backup.service ./demo-backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If verification succeeds cleanly, the command prints nothing and exits successfully.&lt;/p&gt;

&lt;p&gt;I like verifying related units in one command because a timer that points at the wrong service name is just as broken as a bad service file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical local workflow before install
&lt;/h2&gt;

&lt;p&gt;When I am editing units by hand, this is the order I prefer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write or update the unit files in a working directory.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;systemd-analyze verify&lt;/code&gt; against the service, timer, socket, or path units involved.&lt;/li&gt;
&lt;li&gt;Copy them into &lt;code&gt;/etc/systemd/system/&lt;/code&gt; only after they verify cleanly.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Start the unit and inspect logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./myjob.service ./myjob.timer &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 ./myjob.service ./myjob.timer /etc/systemd/system/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myjob.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then confirm both the unit state and recent logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myjob.timer myjob.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.service &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.timer &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make warnings fail CI with &lt;code&gt;--recursive-errors=&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One subtle detail from the manual matters a lot for automation.&lt;/p&gt;

&lt;p&gt;If you do not pass &lt;code&gt;--recursive-errors=&lt;/code&gt;, &lt;code&gt;systemd-analyze verify&lt;/code&gt; may still print warnings while returning a zero exit status.&lt;/p&gt;

&lt;p&gt;For CI or packaging checks, use one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify &lt;span class="nt"&gt;--recursive-errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; ./myjob.service ./myjob.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yes&lt;/code&gt;: fail on warnings in the unit or any associated dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;one&lt;/code&gt;: fail on warnings in the unit or its immediate dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no&lt;/code&gt;: fail only on warnings in the explicitly specified unit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most CI checks, I would choose &lt;code&gt;yes&lt;/code&gt; if the build environment contains the full dependency set, or &lt;code&gt;one&lt;/code&gt; if you want a stricter signal on the files you directly touched without turning unrelated environment noise into failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying staged files in a package or image root
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze&lt;/code&gt; also supports &lt;code&gt;--root=PATH&lt;/code&gt; for verification against a different filesystem tree.&lt;/p&gt;

&lt;p&gt;That is useful when you build packages, chroots, or machine images and want to validate units before they land on the live host.&lt;/p&gt;

&lt;p&gt;Example layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkgroot/
└── etc/systemd/system/
    └── app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/pkgroot"&lt;/span&gt; app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical warning here: this works best when the alternate root actually contains the unit dependencies and executable paths your unit references. If the staged root is too minimal, you can get errors about missing units or binaries that exist on the final system but not inside the staging tree.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;--root=&lt;/code&gt; is excellent for representative chroots and image roots, but less useful on a skeletal directory tree that only contains one unit file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;verify&lt;/code&gt; does not replace
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze verify&lt;/code&gt; is valuable, but it is not the whole test plan.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your service logic is correct&lt;/li&gt;
&lt;li&gt;the command behaves correctly with real environment variables or credentials&lt;/li&gt;
&lt;li&gt;the service has all required runtime permissions&lt;/li&gt;
&lt;li&gt;the timer schedule is what you intended&lt;/li&gt;
&lt;li&gt;the service will stay healthy after startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a clean verify, I still recommend testing the real activation path.&lt;/p&gt;

&lt;p&gt;For timer units, this is especially useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze calendar &lt;span class="s1"&gt;'03:15'&lt;/span&gt;
systemctl start myjob.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.service &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That way you validate both the unit syntax and the real runtime behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple repo-friendly check script
&lt;/h2&gt;

&lt;p&gt;If you keep your units in Git, add a small verifier script:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;units&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  systemd/myjob.service
  systemd/myjob.timer
&lt;span class="o"&gt;)&lt;/span&gt;

systemd-analyze verify &lt;span class="nt"&gt;--recursive-errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;one &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it locally before commits, or in CI before packaging and deployment.&lt;/p&gt;

&lt;p&gt;For a GitHub Actions step, the core check is as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify systemd units&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;systemd-analyze verify --recursive-errors=one \&lt;/span&gt;
      &lt;span class="s"&gt;systemd/myjob.service \&lt;/span&gt;
      &lt;span class="s"&gt;systemd/myjob.timer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one step catches a surprising number of avoidable mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;If you work with &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;systemd-analyze verify&lt;/code&gt; is one of those small tools that pays for itself fast.&lt;/p&gt;

&lt;p&gt;It will not replace actually starting the service, but it is excellent at catching the boring, expensive mistakes early: typos, wrong dependency names, and broken command paths.&lt;/p&gt;

&lt;p&gt;My rule of thumb is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify before install&lt;/li&gt;
&lt;li&gt;reload only after verify passes&lt;/li&gt;
&lt;li&gt;start the unit and inspect logs before calling it done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns unit-file edits from guesswork into a repeatable workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-analyze(1)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux man page mirror for &lt;code&gt;systemd-analyze(1)&lt;/code&gt;: &lt;a href="https://man7.org/linux/man-pages/man1/systemd-analyze.1.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man1/systemd-analyze.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.unit(5)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.timer(5)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl(1)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemctl.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemctl.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Cloning Stale Hostnames: Practical `systemd-firstboot` for Linux Images</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 01 May 2026 02:02:48 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-cloning-stale-hostnames-practical-systemd-firstboot-for-linux-images-bjp</link>
      <guid>https://dev.to/lyraalishaikh/stop-cloning-stale-hostnames-practical-systemd-firstboot-for-linux-images-bjp</guid>
      <description>&lt;p&gt;If you build Linux images for VMs, lab machines, edge devices, or golden templates, you have probably hit the same mess at least once.&lt;/p&gt;

&lt;p&gt;You clone an image, boot it, and realize it still carries a stale hostname, the wrong timezone, or a machine identity you never meant to duplicate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; is a small tool that solves exactly that class of problem. It writes first-boot configuration directly into an offline root filesystem or disk image, before the system ever starts.&lt;/p&gt;

&lt;p&gt;That makes it useful when you want image builds to stay reproducible, but you still need a clean way to initialize the parts that should be unique or environment-specific.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical workflow for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setting locale, timezone, and hostname in an offline image&lt;/li&gt;
&lt;li&gt;generating a fresh machine ID correctly&lt;/li&gt;
&lt;li&gt;pre-seeding root access without putting a plaintext password on the command line&lt;/li&gt;
&lt;li&gt;resetting first-boot state when you want an image to ask again&lt;/li&gt;
&lt;li&gt;verifying what changed before you ship the image&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why use &lt;code&gt;systemd-firstboot&lt;/code&gt; instead of editing files yourself?
&lt;/h2&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; write &lt;code&gt;/etc/hostname&lt;/code&gt;, &lt;code&gt;/etc/locale.conf&lt;/code&gt;, &lt;code&gt;/etc/machine-id&lt;/code&gt;, and &lt;code&gt;/etc/localtime&lt;/code&gt; by hand.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;systemd-firstboot&lt;/code&gt; gives you a few advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it understands both offline root directories and disk images&lt;/li&gt;
&lt;li&gt;it knows which files correspond to each setting&lt;/li&gt;
&lt;li&gt;it avoids overwriting existing values unless you explicitly ask it to&lt;/li&gt;
&lt;li&gt;it can generate a fresh machine ID for an offline image&lt;/li&gt;
&lt;li&gt;it has a supported reset workflow for returning an image to first-boot state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also operates directly on the filesystem, without needing the target system to be booted. That is the key difference from tools like &lt;code&gt;hostnamectl&lt;/code&gt;, &lt;code&gt;timedatectl&lt;/code&gt;, or &lt;code&gt;localectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can configure
&lt;/h2&gt;

&lt;p&gt;According to the upstream manual, &lt;code&gt;systemd-firstboot&lt;/code&gt; can initialize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;machine ID&lt;/li&gt;
&lt;li&gt;locale and message locale&lt;/li&gt;
&lt;li&gt;keyboard map&lt;/li&gt;
&lt;li&gt;timezone&lt;/li&gt;
&lt;li&gt;hostname&lt;/li&gt;
&lt;li&gt;kernel command line used by &lt;code&gt;kernel-install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;root password and root shell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a solid set of knobs for image preparation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Initialize an offline root directory
&lt;/h2&gt;

&lt;p&gt;Let’s start with the simplest case: you have a mounted root filesystem at &lt;code&gt;/mnt/golden-root&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;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writes &lt;code&gt;/mnt/golden-root/etc/locale.conf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creates the &lt;code&gt;/mnt/golden-root/etc/localtime&lt;/code&gt; symlink&lt;/li&gt;
&lt;li&gt;writes &lt;code&gt;/mnt/golden-root/etc/hostname&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creates &lt;code&gt;/mnt/golden-root/etc/machine-id&lt;/code&gt; with a random ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick verification pass:&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;sudo cat&lt;/span&gt; /mnt/golden-root/etc/locale.conf
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /mnt/golden-root/etc/hostname
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /mnt/golden-root/etc/machine-id
&lt;span class="nb"&gt;sudo readlink&lt;/span&gt; /mnt/golden-root/etc/localtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected shape of the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LANG=en_US.UTF-8
web-template
3d6f5d6d8b714d55a78f55c9e08b0d47
../usr/share/zoneinfo/UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2: Work directly on a disk image
&lt;/h2&gt;

&lt;p&gt;If your build pipeline produces a raw disk image instead of a mounted root directory, &lt;code&gt;--image=&lt;/code&gt; is usually more convenient.&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./debian-golden.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially handy in image-building workflows where you do not want to mount partitions manually first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important machine ID rule
&lt;/h2&gt;

&lt;p&gt;The machine ID should be unique per instance.&lt;/p&gt;

&lt;p&gt;If you ship multiple clones with the same populated &lt;code&gt;/etc/machine-id&lt;/code&gt;, some software will treat them as the same machine identity. That can cause confusing behavior in logs, telemetry, or service registration.&lt;/p&gt;

&lt;p&gt;For offline images, use one of these patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: generate one during image preparation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when the image itself is the final deployed system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: reset first-boot-managed files so the target config happens on first boot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--reset&lt;/code&gt; option removes files managed by &lt;code&gt;systemd-firstboot&lt;/code&gt;, so the next boot is treated as first boot again.&lt;/p&gt;

&lt;p&gt;I like this pattern for reusable templates that should be finalized only after cloning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Seed a root password without exposing plaintext in &lt;code&gt;ps&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The manual explicitly warns against placing plaintext passwords on the command line, because other users may be able to see them via &lt;code&gt;ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A safer workflow is to pass a hashed password.&lt;/p&gt;

&lt;p&gt;Generate a SHA-512 hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl passwd &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted for the password instead of placing it in shell history.&lt;/p&gt;

&lt;p&gt;Then apply it to the offline image:&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root-password-hashed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$6$rounds=10000$REPLACE_WITH_REAL_HASH'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need fully non-interactive automation, store the hash in your secret manager or CI secret store and inject it at runtime.&lt;/p&gt;

&lt;p&gt;Afterward, verify that &lt;code&gt;passwd&lt;/code&gt; and &lt;code&gt;shadow&lt;/code&gt; were created inside the target root:&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;sudo ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /mnt/golden-root/etc/passwd /mnt/golden-root/etc/shadow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 4: Copy host settings, but be selective
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; can copy some settings from the build host.&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--copy-locale&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--copy-timezone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is convenient, but I would use it carefully.&lt;/p&gt;

&lt;p&gt;For reproducible image builds, explicit values are usually better than inheriting whatever happens to be configured on the build machine that day.&lt;/p&gt;

&lt;p&gt;Good use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local lab image built on a trusted workstation, where host timezone and locale are intentional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less good use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI runners or shared build hosts, where inherited settings may vary silently&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example 5: Force an update when files already exist
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;systemd-firstboot&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; overwrite existing configuration files.&lt;/p&gt;

&lt;p&gt;That is a good default, but it can surprise you if you are iterating on an image and nothing seems to change.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;--force&lt;/code&gt; when you really do want replacement behavior:&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web-prod-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Europe/Berlin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--force&lt;/code&gt;, existing files are left alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical golden-image workflow
&lt;/h2&gt;

&lt;p&gt;Here is a pattern I trust for VM templates and appliance-style images.&lt;/p&gt;

&lt;h3&gt;
  
  
  During image build
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install packages and application bits.&lt;/li&gt;
&lt;li&gt;Set stable defaults that should be common everywhere.&lt;/li&gt;
&lt;li&gt;Leave machine-specific values for first-boot time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;template-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Before sealing the template
&lt;/h3&gt;

&lt;p&gt;Reset first-boot-managed files if clones should personalize later:&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;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After clone or deployment
&lt;/h3&gt;

&lt;p&gt;Either let &lt;code&gt;systemd-firstboot.service&lt;/code&gt; prompt on first boot where appropriate, or inject settings during provisioning.&lt;/p&gt;

&lt;p&gt;That split keeps the image generic while still using supported systemd-native tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-firstboot&lt;/code&gt; is not for
&lt;/h2&gt;

&lt;p&gt;A few boundaries matter here.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; use it as a general configuration-management replacement. It is not Ansible, not cloud-init, and not a full provisioning engine.&lt;/p&gt;

&lt;p&gt;It is best for basic early identity and boot-adjacent settings.&lt;/p&gt;

&lt;p&gt;Also, it is not recommended as your normal interface for changing a running system that is already configured. For live systems, use the regular tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hostnamectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timedatectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;localectl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--setup-machine-id&lt;/code&gt; does nothing on a live system
&lt;/h3&gt;

&lt;p&gt;That is expected. The manual notes that machine ID setup with &lt;code&gt;--setup-machine-id&lt;/code&gt; is for use with &lt;code&gt;--root=&lt;/code&gt; or &lt;code&gt;--image=&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--root-shell&lt;/code&gt; fails for an offline root
&lt;/h3&gt;

&lt;p&gt;The shell path must exist inside the target root. If your image does not contain &lt;code&gt;/bin/bash&lt;/code&gt;, setting &lt;code&gt;--root-shell=/bin/bash&lt;/code&gt; will fail.&lt;/p&gt;

&lt;p&gt;Verify first:&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;sudo test&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; /mnt/golden-root/bin/bash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;--reset&lt;/code&gt; seems aggressive
&lt;/h3&gt;

&lt;p&gt;It is. &lt;code&gt;--reset&lt;/code&gt; removes files configured by &lt;code&gt;systemd-firstboot&lt;/code&gt; so the next boot is treated as first boot again. Use it intentionally, ideally near the end of an image pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; is one of those tools that feels small until you start building reusable Linux images regularly.&lt;/p&gt;

&lt;p&gt;Then it becomes a very clean answer to a real operational problem: how do you prepare an image &lt;em&gt;without&lt;/em&gt; baking in the identity that should only exist after deployment?&lt;/p&gt;

&lt;p&gt;If you are shipping templates, appliances, lab VMs, or self-hosted images, it is worth adding to your toolbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;systemd upstream manual, &lt;code&gt;systemd-firstboot(1)&lt;/code&gt;: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage mirror for &lt;code&gt;systemd-firstboot(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ArchWiki overview: &lt;a href="https://wiki.archlinux.org/title/Systemd-firstboot" rel="noopener noreferrer"&gt;https://wiki.archlinux.org/title/Systemd-firstboot&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Hand-Crafting Service Users: Practical `systemd-sysusers` for Declarative Linux Accounts</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 28 Apr 2026 05:02:41 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-crafting-service-users-practical-systemd-sysusers-for-declarative-linux-accounts-2l7a</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-crafting-service-users-practical-systemd-sysusers-for-declarative-linux-accounts-2l7a</guid>
      <description>&lt;p&gt;If you still create service accounts with ad hoc &lt;code&gt;useradd&lt;/code&gt; commands in install scripts or README files, you are making something important harder to audit, harder to override, and easier to forget.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; gives you a declarative way to create &lt;strong&gt;system users and groups&lt;/strong&gt; from simple config files. It is built for service identities, package installs, image builds, and first-boot provisioning, not for interactive human accounts.&lt;/p&gt;

&lt;p&gt;In this guide, I will show how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define service users and groups with &lt;code&gt;sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;validate changes safely with &lt;code&gt;--dry-run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;understand override precedence between &lt;code&gt;/usr/lib&lt;/code&gt; and &lt;code&gt;/etc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;--replace&lt;/code&gt; for packaging workflows&lt;/li&gt;
&lt;li&gt;pair account creation with the right directory-management approach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All commands and behaviors below were checked against the current &lt;code&gt;systemd-sysusers(8)&lt;/code&gt; and &lt;code&gt;sysusers.d(5)&lt;/code&gt; documentation, plus test runs on a Linux host with systemd 257.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-sysusers&lt;/code&gt; is for, and what it is not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; creates &lt;strong&gt;system users and groups&lt;/strong&gt; and adds users to groups based on declarative config.&lt;/p&gt;

&lt;p&gt;It is a good fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;daemon accounts like &lt;code&gt;_demoapp&lt;/code&gt; or &lt;code&gt;postgres&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;package install or image-build workflows&lt;/li&gt;
&lt;li&gt;reproducible service identities across machines&lt;/li&gt;
&lt;li&gt;first-boot or offline-root provisioning with &lt;code&gt;--root&lt;/code&gt; or &lt;code&gt;--image&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; the right tool for normal login users. The man page is explicit that it is for system users and groups, and it works directly with local account files like &lt;code&gt;/etc/passwd&lt;/code&gt; and &lt;code&gt;/etc/group&lt;/code&gt; rather than remote identity sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is better than &lt;code&gt;useradd&lt;/code&gt; in random scripts
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;sysusers.d&lt;/code&gt; file is easier to reason about than an imperative shell snippet because it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;declarative&lt;/strong&gt;, the desired account state lives in a file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auditable&lt;/strong&gt;, you can see exactly which identities a package or service expects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;override-friendly&lt;/strong&gt;, admins can replace vendor defaults in &lt;code&gt;/etc/sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;testable&lt;/strong&gt;, &lt;code&gt;systemd-sysusers --dry-run&lt;/code&gt; shows what would happen before anything is written&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it especially useful for service packaging and reproducible infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  File locations and precedence
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sysusers.d&lt;/code&gt; files are read from these locations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/sysusers.d/*.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/sysusers.d/*.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/sysusers.d/*.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/sysusers.d/*.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important rule is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/sysusers.d&lt;/code&gt; overrides &lt;code&gt;/run/sysusers.d&lt;/code&gt; and &lt;code&gt;/usr/lib/sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/run/sysusers.d&lt;/code&gt; overrides &lt;code&gt;/usr/lib/sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;vendor packages should install files in &lt;code&gt;/usr/lib/sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;local admin overrides belong in &lt;code&gt;/etc/sysusers.d&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to disable a vendor file entirely, the documented approach is to place a symlink to &lt;code&gt;/dev/null&lt;/code&gt; in &lt;code&gt;/etc/sysusers.d&lt;/code&gt; with the same filename.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format, one line at a time
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;sysusers.d&lt;/code&gt; file is line-oriented. The most common record types are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;u&lt;/code&gt; to create a system user, and implicitly a same-named group&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g&lt;/code&gt; to create a group&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;m&lt;/code&gt; to add a user to a group&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;r&lt;/code&gt; to define a UID/GID allocation range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a practical example for a daemon called &lt;code&gt;demoapp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /usr/lib/sysusers.d/demoapp.conf
&lt;/span&gt;&lt;span class="err"&gt;u!&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;"Demo&lt;/span&gt; &lt;span class="err"&gt;app&lt;/span&gt; &lt;span class="err"&gt;service&lt;/span&gt; &lt;span class="err"&gt;user"&lt;/span&gt;
&lt;span class="err"&gt;g&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;
&lt;span class="err"&gt;m&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates a locked system user named &lt;code&gt;_demoapp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;lets systemd allocate a UID automatically&lt;/li&gt;
&lt;li&gt;creates a supplemental group called &lt;code&gt;demoapp-data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;adds &lt;code&gt;_demoapp&lt;/code&gt; to that group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;u!&lt;/code&gt; is preferable for most daemon accounts because it creates a &lt;strong&gt;fully locked&lt;/strong&gt; account&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; in the ID field means automatic UID/GID allocation&lt;/li&gt;
&lt;li&gt;prefixing service accounts with &lt;code&gt;_&lt;/code&gt; is strongly recommended by the docs to avoid clashes with human users&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validate before touching the real system
&lt;/h2&gt;

&lt;p&gt;My favorite part of this workflow is that you can test it safely.&lt;/p&gt;

&lt;p&gt;Example:&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="nv"&gt;tmpdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;etc,usr/lib&lt;span class="o"&gt;}&lt;/span&gt;/sysusers.d

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/sysusers.d/demoapp.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
u! _demoapp - "Demo app service user"
g demoapp-data -
m _demoapp demoapp-data
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemd-sysusers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/sysusers.d/demoapp.conf"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my test run, this reported output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creating group 'demoapp-data' with GID 999.
Creating group '_demoapp' with GID 998.
Creating user '_demoapp' (Demo app service user) with UID 998 and GID 998.
Would write /etc/group…
Would write /etc/gshadow…
Would write /etc/passwd…
Would write /etc/shadow…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a no-surprises review step for CI, image builds, or packaging checks.&lt;/p&gt;

&lt;p&gt;When you are done testing, clean up the temp root:&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Admin override example
&lt;/h2&gt;

&lt;p&gt;Suppose a vendor ships this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /usr/lib/sysusers.d/demoapp.conf
&lt;/span&gt;&lt;span class="err"&gt;u!&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;"Demo&lt;/span&gt; &lt;span class="err"&gt;app&lt;/span&gt; &lt;span class="err"&gt;service&lt;/span&gt; &lt;span class="err"&gt;user"&lt;/span&gt;
&lt;span class="err"&gt;g&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;
&lt;span class="err"&gt;m&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you want a fixed UID and GID locally for a migration or NFS-mapped storage policy.&lt;/p&gt;

&lt;p&gt;Create an override in &lt;code&gt;/etc/sysusers.d/demoapp.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;u!&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;450&lt;/span&gt; &lt;span class="err"&gt;"Demo&lt;/span&gt; &lt;span class="err"&gt;app&lt;/span&gt; &lt;span class="err"&gt;service&lt;/span&gt; &lt;span class="err"&gt;user"&lt;/span&gt;
&lt;span class="err"&gt;g&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt; &lt;span class="err"&gt;451&lt;/span&gt;
&lt;span class="err"&gt;m&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;demoapp-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test the effective result without writing anything:&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="nv"&gt;tmpdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;etc,usr/lib&lt;span class="o"&gt;}&lt;/span&gt;/sysusers.d

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/sysusers.d/demoapp.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
u! _demoapp - "Demo app service user"
g demoapp-data -
m _demoapp demoapp-data
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;/etc/sysusers.d/demoapp.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
u! _demoapp 450 "Demo app service user"
g demoapp-data 451
m _demoapp demoapp-data
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt; demoapp.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my test run, the override won and &lt;code&gt;systemd-sysusers&lt;/code&gt; planned to create &lt;code&gt;_demoapp&lt;/code&gt; with UID 450 and &lt;code&gt;demoapp-data&lt;/code&gt; with GID 451.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of behavior you want in a packaging-friendly declarative system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline testing is handy for quick experiments
&lt;/h2&gt;

&lt;p&gt;If you just want to test a few rules without writing a file first, &lt;code&gt;--inline&lt;/code&gt; is useful:&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="nv"&gt;tmpdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;/etc/sysusers.d"&lt;/span&gt;

systemd-sysusers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmpdir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--inline&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'u! _cachebot - "Cache Bot" /var/lib/cachebot /usr/sbin/nologin'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'g cachebot-data -'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'m _cachebot cachebot-data'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;experimenting during packaging work&lt;/li&gt;
&lt;li&gt;verifying syntax in CI&lt;/li&gt;
&lt;li&gt;teaching teammates what each field does&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;--replace&lt;/code&gt; is built for packaging workflows
&lt;/h2&gt;

&lt;p&gt;One easy-to-miss feature is &lt;code&gt;--replace=PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This matters when a package installation script needs to create accounts &lt;strong&gt;before&lt;/strong&gt; its final &lt;code&gt;sysusers.d&lt;/code&gt; file is present on disk. The man page includes this exact pattern:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'u! _radvd - "radvd daemon"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | systemd-sysusers &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/sysusers.d/radvd.conf -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells &lt;code&gt;systemd-sysusers&lt;/code&gt; to treat the supplied content as if it were replacing &lt;code&gt;/usr/lib/sysusers.d/radvd.conf&lt;/code&gt;, while still respecting any higher-priority admin override that may already exist in &lt;code&gt;/etc/sysusers.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a subtle feature, but a very useful one for package maintainers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important limitation: this does not create your data directories
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; creates account entries. It does &lt;strong&gt;not&lt;/strong&gt; create the service's state directories for you.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sysusers.d(5)&lt;/code&gt; docs explicitly recommend pairing it with the right directory mechanism.&lt;/p&gt;

&lt;p&gt;For modern systemd services, prefer unit-level directory management when possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/demoapp.service
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;_demoapp&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;_demoapp&lt;/span&gt;
&lt;span class="py"&gt;StateDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demoapp&lt;/span&gt;
&lt;span class="py"&gt;CacheDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demoapp&lt;/span&gt;
&lt;span class="py"&gt;LogsDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demoapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually cleaner than a separate tmpfiles rule because the directory lifecycle stays attached to the service definition.&lt;/p&gt;

&lt;p&gt;If you really need a separate tmpfiles policy, use &lt;code&gt;tmpfiles.d&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /usr/lib/tmpfiles.d/demoapp.conf
&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt; &lt;span class="err"&gt;/var/lib/demoapp&lt;/span&gt; &lt;span class="err"&gt;0750&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;
&lt;span class="err"&gt;d&lt;/span&gt; &lt;span class="err"&gt;/var/log/demoapp&lt;/span&gt; &lt;span class="err"&gt;0750&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;_demoapp&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply or test it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/demoapp &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/demoapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A few practical rules I would follow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use automatic UID/GID allocation unless you truly need fixed numbers.&lt;/strong&gt; The docs strongly recommend &lt;code&gt;-&lt;/code&gt; for most cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix daemon accounts with &lt;code&gt;_&lt;/code&gt;.&lt;/strong&gt; This reduces collision risk with human users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer &lt;code&gt;u!&lt;/code&gt; for service identities.&lt;/strong&gt; Locked accounts are safer for daemons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep vendor config in &lt;code&gt;/usr/lib/sysusers.d&lt;/code&gt;, local policy in &lt;code&gt;/etc/sysusers.d&lt;/code&gt;.&lt;/strong&gt; That is the intended split.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair accounts with service-level directories or tmpfiles.&lt;/strong&gt; Do not assume the home or state path will magically appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;--dry-run&lt;/code&gt; in CI and image builds.&lt;/strong&gt; It is cheap and catches bad assumptions early.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to use &lt;code&gt;DynamicUser=&lt;/code&gt; instead
&lt;/h2&gt;

&lt;p&gt;If your service does not need a persistent, named account in &lt;code&gt;/etc/passwd&lt;/code&gt;, consider &lt;code&gt;DynamicUser=&lt;/code&gt; in the service unit instead.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;systemd-sysusers&lt;/code&gt; when you want a stable, declarative &lt;strong&gt;real system identity&lt;/strong&gt;. Use &lt;code&gt;DynamicUser=&lt;/code&gt; when you want systemd to allocate a temporary runtime identity for a service.&lt;/p&gt;

&lt;p&gt;That distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysusers&lt;/code&gt; is better for packages, shared file ownership, and predictable account names&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DynamicUser=&lt;/code&gt; is better for tighter isolation when persistence is unnecessary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If you care about reproducible Linux systems, service identities should live in configuration, not in half-remembered setup commands.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; is one of those small systemd tools that quietly removes a lot of operational mess. You get clearer intent, safer testing, and a much better override story than hand-written account creation scripts.&lt;/p&gt;

&lt;p&gt;For daemon users, that is a solid trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysusers(8)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man8/systemd-sysusers.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/systemd-sysusers.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sysusers.d(5)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man5/sysusers.d.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/sysusers.d.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd upstream notes on UID/GID ranges: &lt;a href="https://systemd.io/UIDS-GIDS/" rel="noopener noreferrer"&gt;https://systemd.io/UIDS-GIDS/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd upstream notes on user/group naming: &lt;a href="https://systemd.io/USER_NAMES/" rel="noopener noreferrer"&gt;https://systemd.io/USER_NAMES/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmpfiles.d(5)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man5/tmpfiles.d.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/tmpfiles.d.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.exec(5)&lt;/code&gt; for &lt;code&gt;StateDirectory=&lt;/code&gt; and related directives: &lt;a href="https://man7.org/linux/man-pages/man5/systemd.exec.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/systemd.exec.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Rebuilding Images for Every Config Change: Practical `systemd-confext` for Portable `/etc` Overlays</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 27 Apr 2026 05:03:06 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-rebuilding-images-for-every-config-change-practical-systemd-confext-for-portable-etc-1n0h</link>
      <guid>https://dev.to/lyraalishaikh/stop-rebuilding-images-for-every-config-change-practical-systemd-confext-for-portable-etc-1n0h</guid>
      <description>&lt;h1&gt;
  
  
  Stop Rebuilding Images for Every Config Change: Practical &lt;code&gt;systemd-confext&lt;/code&gt; for Portable &lt;code&gt;/etc&lt;/code&gt; Overlays
&lt;/h1&gt;

&lt;p&gt;If your base OS image is supposed to stay stable, config drift becomes annoying fast.&lt;/p&gt;

&lt;p&gt;You tweak a unit drop-in, change a tmpfiles rule, add a sysctl file, or adjust journald settings, and suddenly you are choosing between three awkward options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;rebuild the whole image,&lt;/li&gt;
&lt;li&gt;mutate &lt;code&gt;/etc&lt;/code&gt; in place and hope you can track it later,&lt;/li&gt;
&lt;li&gt;bolt on a one-off config management path just for a small policy change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;systemd-confext&lt;/code&gt; gives you another option.&lt;/p&gt;

&lt;p&gt;It lets you ship a version-checked, read-only overlay for &lt;code&gt;/etc&lt;/code&gt;, then merge or unmerge it at runtime. In practice, that means you can package configuration as a portable layer, apply it without rebuilding the full OS image, and roll it back by removing the layer.&lt;/p&gt;

&lt;p&gt;This post is intentionally &lt;strong&gt;not&lt;/strong&gt; about &lt;code&gt;systemd-sysext&lt;/code&gt; for &lt;code&gt;/usr&lt;/code&gt;. I covered that separately. Here the focus is narrower and more operational: &lt;strong&gt;portable &lt;code&gt;/etc&lt;/code&gt; overlays for runtime reconfiguration&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-confext&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-confext&lt;/code&gt; is the configuration-extension companion to &lt;code&gt;systemd-sysext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Where &lt;code&gt;systemd-sysext&lt;/code&gt; overlays &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt;, &lt;code&gt;systemd-confext&lt;/code&gt; overlays &lt;strong&gt;only &lt;code&gt;/etc&lt;/code&gt;&lt;/strong&gt;. Extension content outside &lt;code&gt;/etc&lt;/code&gt; is ignored. The merged &lt;code&gt;/etc&lt;/code&gt; overlay is mounted with &lt;code&gt;nosuid&lt;/code&gt; and, by default, &lt;code&gt;noexec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few practical consequences matter immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is for &lt;strong&gt;configuration&lt;/strong&gt;, not for shipping binaries,&lt;/li&gt;
&lt;li&gt;it is &lt;strong&gt;read-only by default&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;it can be &lt;strong&gt;merged, unmerged, refreshed, listed, and inspected&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;compatibility is checked against the base OS before merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to the upstream man page, &lt;code&gt;systemd-confext&lt;/code&gt; support was added in &lt;strong&gt;systemd 254&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is a good fit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-confext&lt;/code&gt; makes sense when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ship a small set of &lt;code&gt;/etc&lt;/code&gt; policy files separately from the base image,&lt;/li&gt;
&lt;li&gt;change service configuration without a full image rebuild,&lt;/li&gt;
&lt;li&gt;keep rollback simple by removing one extension layer,&lt;/li&gt;
&lt;li&gt;test configuration overlays against an offline root with &lt;code&gt;--root=&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;journald.conf.d/&lt;/code&gt; retention policy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmpfiles.d/&lt;/code&gt; cleanup rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sysctl.d/&lt;/code&gt; tuning profiles&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd/system/*.service.d/&lt;/code&gt; drop-ins&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;modprobe.d/&lt;/code&gt; policy files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh/sshd_config.d/&lt;/code&gt; overlays, if your distro uses drop-ins&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; treat &lt;code&gt;systemd-confext&lt;/code&gt; as a generic packaging system.&lt;/p&gt;

&lt;p&gt;It is a poor fit when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependency management,&lt;/li&gt;
&lt;li&gt;earliest-boot configuration before the relevant filesystems are available,&lt;/li&gt;
&lt;li&gt;service payloads or binaries, which belong in packages, portable services, or &lt;code&gt;systemd-sysext&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, avoid using it to replace every normal config-management workflow. This is strongest when you want a &lt;strong&gt;portable, reversible config layer&lt;/strong&gt; tied to an image-based or tightly controlled host setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check whether your host supports it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-confext &lt;span class="nt"&gt;--version&lt;/span&gt;
systemd-confext &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the command is missing, your systemd build is probably older than 254 or your distro has not shipped the tool yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How matching works
&lt;/h2&gt;

&lt;p&gt;A confext image must include a metadata file named like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/extension-release.d/extension-release.NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NAME&lt;/code&gt; must match the confext image name.&lt;/p&gt;

&lt;p&gt;That file follows the &lt;code&gt;os-release&lt;/code&gt; style key/value format and is used to verify compatibility with the base OS. For confext images, the important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ID=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VERSION_ID=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CONFEXT_LEVEL=&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain English, your overlay should say what OS it matches. If the compatibility data does not match, &lt;code&gt;systemd-confext&lt;/code&gt; refuses to merge unless you force it.&lt;/p&gt;

&lt;p&gt;That is a feature, not friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where confexts live
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-confext&lt;/code&gt; looks for configuration extensions in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/run/confexts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/confexts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/confexts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/confexts/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, &lt;code&gt;/var/lib/confexts/&lt;/code&gt; is the usual place to install them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical directory-based example
&lt;/h2&gt;

&lt;p&gt;Let’s build a confext that does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;caps persistent journal size,&lt;/li&gt;
&lt;li&gt;adds a systemd drop-in for a service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am using &lt;code&gt;nginx.service&lt;/code&gt; as the example target because the drop-in pattern is common, but the same structure works for any service that already exists on the host.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Create the confext tree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/confexts/ops-policy/etc/extension-release.d
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Add the compatibility metadata
&lt;/h3&gt;

&lt;p&gt;First check the host values:&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;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'ID=%s\nVERSION_ID=%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create the extension metadata file:&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;sudo tee&lt;/span&gt; /var/lib/confexts/ops-policy/etc/extension-release.d/extension-release.ops-policy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
ID=debian
VERSION_ID=12
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust those values for your real host OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Add real config files
&lt;/h3&gt;

&lt;p&gt;A journald policy drop-in:&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;sudo tee&lt;/span&gt; /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d/10-retention.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Journal]
SystemMaxUse=1G
SystemKeepFree=500M
MaxRetentionSec=1month
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a service drop-in:&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;sudo tee&lt;/span&gt; /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d/10-restart-window.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Service]
Restart=on-failure
RestartSec=5s
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Inspect what is installed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext list
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5) Merge the overlay
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the extension metadata requests a manager reload with &lt;code&gt;EXTENSION_RELOAD_MANAGER=1&lt;/code&gt;, the tool can reload the manager automatically unless you pass &lt;code&gt;--no-reload&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6) Verify the result
&lt;/h3&gt;

&lt;p&gt;Check that the overlay is active:&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;sudo &lt;/span&gt;systemd-confext status
mount | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;' on /etc '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the visible files:&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;sudo cat&lt;/span&gt; /etc/systemd/journald.conf.d/10-retention.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;nginx.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the journald config, reload and confirm:&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;sudo &lt;/span&gt;systemctl restart systemd-journald
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;--disk-usage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the service drop-in, reload unit files and inspect the merged unit:&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;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl show nginx.service &lt;span class="nt"&gt;-p&lt;/span&gt; Restart &lt;span class="nt"&gt;-p&lt;/span&gt; RestartUSec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rollback is the nice part
&lt;/h2&gt;

&lt;p&gt;If you remove the overlay, the old files disappear with it.&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;sudo &lt;/span&gt;systemd-confext unmerge
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the operational appeal here. You are not cleaning up a pile of hand-edited files in &lt;code&gt;/etc&lt;/code&gt;. You are withdrawing one layer.&lt;/p&gt;

&lt;p&gt;If you change files inside the extension tree, use:&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;sudo &lt;/span&gt;systemd-confext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be aware that upstream documents &lt;code&gt;refresh&lt;/code&gt; as an unmerge followed by a merge, with a brief gap where the overlaid files disappear before the new overlay is mounted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer testing with &lt;code&gt;--root=&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the best features for real operations is &lt;code&gt;--root=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can test against an offline root directory instead of your live host &lt;code&gt;/etc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example layout:&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;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/testroot/etc
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/os-release /tmp/testroot/etc/
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /var/lib/confexts /tmp/testroot/var/lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then operate on the alternate root:&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;sudo &lt;/span&gt;systemd-confext &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/testroot status
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/testroot merge
find /tmp/testroot/etc &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 4 | &lt;span class="nb"&gt;sort
sudo &lt;/span&gt;systemd-confext &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/testroot unmerge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when you build images in CI or want to validate a confext against a mounted image root before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  About mutability
&lt;/h2&gt;

&lt;p&gt;By default, a merged confext makes &lt;code&gt;/etc&lt;/code&gt; read-only.&lt;/p&gt;

&lt;p&gt;That default is usually what you want, because it preserves the idea that the overlay is controlled and reproducible.&lt;/p&gt;

&lt;p&gt;If you need writes while extensions are merged, &lt;code&gt;systemd-confext&lt;/code&gt; supports mutable modes through &lt;code&gt;--mutable=&lt;/code&gt;. Upstream documents these modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no&lt;/code&gt; (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ephemeral&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ephemeral-import&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These modes route writes through &lt;code&gt;/var/lib/extensions.mutable/&lt;/code&gt; or ephemeral directories, depending on the mode.&lt;/p&gt;

&lt;p&gt;My advice: start with the default immutable behavior unless you have a very specific write-routing need. The more mutable your overlay becomes, the less clean your rollback story is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disk-image workflows
&lt;/h2&gt;

&lt;p&gt;Confexts do not have to be plain directories. The same interface also supports image-based extensions, and the upstream &lt;code&gt;systemd-repart&lt;/code&gt; documentation explicitly mentions &lt;code&gt;--make-ddi=TYPE&lt;/code&gt; with &lt;code&gt;confext&lt;/code&gt; as one of the generated image types.&lt;/p&gt;

&lt;p&gt;That is useful when you want signed or image-based delivery instead of a plain directory tree, but directory-based confexts are the easiest place to start because they are simple to inspect and debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes to avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Shipping the wrong path
&lt;/h3&gt;

&lt;p&gt;If your files are not under &lt;code&gt;/etc&lt;/code&gt; inside the confext tree, they will not be merged.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Forgetting the metadata file
&lt;/h3&gt;

&lt;p&gt;No valid &lt;code&gt;extension-release.NAME&lt;/code&gt;, no clean compatibility check.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Mismatching the image name
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;NAME&lt;/code&gt; in &lt;code&gt;extension-release.NAME&lt;/code&gt; must match the extension image name.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Using it for binaries
&lt;/h3&gt;

&lt;p&gt;That is what &lt;code&gt;systemd-sysext&lt;/code&gt; or packages are for.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Assuming &lt;code&gt;refresh&lt;/code&gt; is atomic
&lt;/h3&gt;

&lt;p&gt;It is convenient, but upstream notes that there is a brief disappearance window during refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pattern I like in practice
&lt;/h2&gt;

&lt;p&gt;For image-based hosts, split changes into three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;base OS image&lt;/strong&gt; for the stable platform,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;systemd-sysext&lt;/code&gt;&lt;/strong&gt; for optional &lt;code&gt;/usr&lt;/code&gt; tools,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;systemd-confext&lt;/code&gt;&lt;/strong&gt; for runtime &lt;code&gt;/etc&lt;/code&gt; policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation keeps intent clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;binaries live in one layer,&lt;/li&gt;
&lt;li&gt;configuration lives in another,&lt;/li&gt;
&lt;li&gt;rollback stays understandable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-confext&lt;/code&gt; is not a replacement for every config-management tool, and it is not the right answer for every Linux host.&lt;/p&gt;

&lt;p&gt;But if you run image-based systems, appliances, lab hosts, or tightly controlled servers, it gives you a very clean operational primitive:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ship config as a removable layer, validate it against the host OS, merge it when needed, and roll it back without leaving &lt;code&gt;/etc&lt;/code&gt; full of debris.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a pretty nice trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;systemd-sysext / systemd-confext man page (man7 mirror): &lt;a href="https://man7.org/linux/man-pages/man8/systemd-sysext.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/systemd-sysext.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os-release&lt;/code&gt; / &lt;code&gt;extension-release&lt;/code&gt; reference (local man page cross-check): &lt;code&gt;man os-release&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;systemd-repart documentation showing DDI generation support for &lt;code&gt;confext&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/systemd/systemd-repart.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd-repart.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd upstream discussion for confext design context: &lt;a href="https://github.com/systemd/systemd/issues/24864" rel="noopener noreferrer"&gt;https://github.com/systemd/systemd/issues/24864&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Shipping Fat Images: Practical `systemd-repart` for First-Boot Partition Growth on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 26 Apr 2026 05:02:42 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-shipping-fat-images-practical-systemd-repart-for-first-boot-partition-growth-on-linux-3m0b</link>
      <guid>https://dev.to/lyraalishaikh/stop-shipping-fat-images-practical-systemd-repart-for-first-boot-partition-growth-on-linux-3m0b</guid>
      <description>&lt;p&gt;If you build Linux images for VMs, cloud instances, or appliances, you usually face the same tradeoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ship a large image that wastes space everywhere, or&lt;/li&gt;
&lt;li&gt;ship a small image and rely on ad hoc first-boot scripts to resize partitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;systemd-repart&lt;/code&gt; gives you a cleaner option. It lets you describe the partitions you want, then grow or add them incrementally at boot or against an image file.&lt;/p&gt;

&lt;p&gt;The useful part is not just automation. It is that the behavior is &lt;strong&gt;declarative, repeatable, and incremental&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical pattern for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;growing the root partition on first boot,&lt;/li&gt;
&lt;li&gt;adding a dedicated &lt;code&gt;/var&lt;/code&gt; partition when extra disk space exists,&lt;/li&gt;
&lt;li&gt;growing the filesystem itself with &lt;code&gt;x-systemd.growfs&lt;/code&gt;, and&lt;/li&gt;
&lt;li&gt;dry-running the whole layout safely against an image file before rollout.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-repart&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-repart(8)&lt;/code&gt; documentation, it is intended for &lt;strong&gt;image-based OS deployments&lt;/strong&gt; and works in a mostly incremental way: it grows existing partitions and adds new ones, but does &lt;strong&gt;not&lt;/strong&gt; shrink, move, or delete partitions during normal operation.&lt;/p&gt;

&lt;p&gt;That matters because it makes first-boot layout changes much less fragile than home-grown &lt;code&gt;parted&lt;/code&gt; scripts.&lt;/p&gt;

&lt;p&gt;A few facts worth keeping straight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-repart&lt;/code&gt; works with &lt;strong&gt;GPT&lt;/strong&gt; partition tables.&lt;/li&gt;
&lt;li&gt;It is typically run in the &lt;strong&gt;initrd at boot&lt;/strong&gt; through &lt;code&gt;systemd-repart.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It can operate on the &lt;strong&gt;running system's backing device&lt;/strong&gt; when invoked without arguments.&lt;/li&gt;
&lt;li&gt;It can also operate on an &lt;strong&gt;image file&lt;/strong&gt; with &lt;code&gt;--image=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;By default, it changes the &lt;strong&gt;partition table&lt;/strong&gt;, not the filesystem inside the partition, unless you explicitly use formatting features or pair it with filesystem growth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one people often miss.&lt;/p&gt;

&lt;p&gt;Growing a root partition is only half the job. You also need the filesystem inside it to grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is a good fit
&lt;/h2&gt;

&lt;p&gt;I like &lt;code&gt;systemd-repart&lt;/code&gt; for cases like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a golden image for KVM, Proxmox, or cloud VMs&lt;/li&gt;
&lt;li&gt;an appliance-style image that should expand to the target disk&lt;/li&gt;
&lt;li&gt;a Linux image where &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, or swap should appear only when space is available&lt;/li&gt;
&lt;li&gt;A/B-style images where the secondary partition is created on first boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would &lt;strong&gt;not&lt;/strong&gt; reach for it first on an already hand-managed server with a messy MBR layout, LVM stack, or years of manual partition edits. This is strongest when the image layout is intentional from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-duplication note
&lt;/h2&gt;

&lt;p&gt;This article intentionally avoids overlap with my recent posts on &lt;code&gt;systemd-sysext&lt;/code&gt;, &lt;code&gt;systemd-delta&lt;/code&gt;, &lt;code&gt;systemd-tmpfiles&lt;/code&gt;, &lt;code&gt;systemd-oomd&lt;/code&gt;, and &lt;code&gt;needrestart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The angle here is specifically &lt;strong&gt;first-boot GPT partition growth and image adaptation&lt;/strong&gt; with &lt;code&gt;systemd-repart&lt;/code&gt;, plus the operational boundary between repartitioning and filesystem growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example layout: small image, bigger disk
&lt;/h2&gt;

&lt;p&gt;Let us say your image ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an ESP&lt;/li&gt;
&lt;li&gt;one root partition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But on the target machine you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the root partition to grow beyond its shipped size&lt;/li&gt;
&lt;li&gt;a separate &lt;code&gt;/var&lt;/code&gt; partition to be created if extra disk space is available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal &lt;code&gt;repart.d&lt;/code&gt; layout could look like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/usr/lib/repart.d/10-root.conf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Partition]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;SizeMinBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2G&lt;/span&gt;
&lt;span class="py"&gt;GrowFileSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;/usr/lib/repart.d/20-var.conf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Partition]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;var&lt;/span&gt;
&lt;span class="py"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;var&lt;/span&gt;
&lt;span class="py"&gt;SizeMinBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4G&lt;/span&gt;
&lt;span class="py"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1000&lt;/span&gt;
&lt;span class="py"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ext4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Type=root&lt;/code&gt; matches the architecture-appropriate GPT root partition type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SizeMinBytes=2G&lt;/code&gt; ensures the root partition is at least 2 GiB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GrowFileSystem=yes&lt;/code&gt; marks the partition so tools that honor the flag grow the filesystem on first mount.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Type=var&lt;/code&gt; declares a &lt;code&gt;/var&lt;/code&gt; partition using the Discoverable Partitions Specification type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Format=ext4&lt;/code&gt; tells &lt;code&gt;systemd-repart&lt;/code&gt; to create a filesystem for that new partition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The matching behavior is filename ordered. The &lt;code&gt;repart.d(5)&lt;/code&gt; docs are explicit that definition files are sorted by filename and matched against existing partitions of the same GPT type in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the filesystem growth explicit
&lt;/h2&gt;

&lt;p&gt;Even though &lt;code&gt;GrowFileSystem=&lt;/code&gt; exists, I prefer making root filesystem growth obvious in the mount configuration too.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;/etc/fstab&lt;/code&gt;, that usually means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LABEL=root  /      ext4  defaults,x-systemd.growfs  0 1
LABEL=var   /var   ext4  defaults                    0 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why I like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is visible during review,&lt;/li&gt;
&lt;li&gt;it makes the filesystem-growth step explicit, and&lt;/li&gt;
&lt;li&gt;it uses the documented &lt;code&gt;systemd-growfs@.service&lt;/code&gt; path that systemd exposes for mounted filesystems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the filesystem is already at full size, &lt;code&gt;systemd-growfs&lt;/code&gt; does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer dry run against an image file
&lt;/h2&gt;

&lt;p&gt;Before baking this into a boot path, test it against an image file.&lt;/p&gt;

&lt;p&gt;Create a working directory:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; repart-demo/repart.d
&lt;span class="nb"&gt;cd &lt;/span&gt;repart-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the definition files:&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; repart.d/10-root.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Partition]
Type=root
Label=root
SizeMinBytes=2G
GrowFileSystem=yes
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; repart.d/20-var.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Partition]
Type=var
Label=var
SizeMinBytes=4G
Weight=1000
Format=ext4
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a dry run against an image file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-repart &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--empty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--definitions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;repart.d &lt;span class="se"&gt;\&lt;/span&gt;
  demo.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new GPT image file is created,&lt;/li&gt;
&lt;li&gt;the partition plan is computed from your &lt;code&gt;.conf&lt;/code&gt; files,&lt;/li&gt;
&lt;li&gt;and no real disk is modified because &lt;code&gt;--dry-run=yes&lt;/code&gt; is in effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the output looks right, you can repeat against a disposable VM image and boot it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A more deterministic fixed-size variant
&lt;/h2&gt;

&lt;p&gt;If you do &lt;strong&gt;not&lt;/strong&gt; want elastic sizing, set the minimum and maximum size to the same value.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Partition]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;var&lt;/span&gt;
&lt;span class="py"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;var&lt;/span&gt;
&lt;span class="py"&gt;SizeMinBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8G&lt;/span&gt;
&lt;span class="py"&gt;SizeMaxBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8G&lt;/span&gt;
&lt;span class="py"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ext4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per &lt;code&gt;repart.d(5)&lt;/code&gt;, when &lt;code&gt;SizeMinBytes=&lt;/code&gt; and &lt;code&gt;SizeMaxBytes=&lt;/code&gt; are equal, the weight no longer matters because the partition size becomes fixed.&lt;/p&gt;

&lt;p&gt;That is useful when you need predictability more than full-disk consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get tripped up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Expecting it to resize filesystems automatically
&lt;/h3&gt;

&lt;p&gt;By default, &lt;code&gt;systemd-repart&lt;/code&gt; is mostly about the &lt;strong&gt;partition table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you enlarge a partition but forget filesystem growth, &lt;code&gt;df -h&lt;/code&gt; may still show the old size.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GrowFileSystem=yes&lt;/code&gt; where appropriate,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-systemd.growfs&lt;/code&gt; for mounts, or&lt;/li&gt;
&lt;li&gt;a documented filesystem-specific growth step if your setup requires it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Using it on the wrong class of system
&lt;/h3&gt;

&lt;p&gt;This is ideal for image-based, GPT-first designs.&lt;/p&gt;

&lt;p&gt;It is not my first choice for a snowflake server with years of manual storage history.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Forgetting that matching is type-based and ordered
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;repart.d&lt;/code&gt; definitions are matched to existing partitions by &lt;strong&gt;GPT type UUID&lt;/strong&gt;, then by filename order among definitions of the same type.&lt;/p&gt;

&lt;p&gt;If you define multiple partitions of the same type, naming and ordering matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Assuming it is destructive by default
&lt;/h3&gt;

&lt;p&gt;Normal &lt;code&gt;systemd-repart&lt;/code&gt; operation is intentionally incremental.&lt;/p&gt;

&lt;p&gt;That said, options like &lt;code&gt;--empty=force&lt;/code&gt; are destructive. Treat those as lab-only until you are absolutely sure what you are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical VM workflow
&lt;/h2&gt;

&lt;p&gt;If I were rolling this out for real, I would use this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;build the image with a deliberately small root partition,&lt;/li&gt;
&lt;li&gt;include &lt;code&gt;repart.d&lt;/code&gt; files in &lt;code&gt;/usr/lib/repart.d/&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;ensure root mounts with &lt;code&gt;x-systemd.growfs&lt;/code&gt; if needed,&lt;/li&gt;
&lt;li&gt;test with &lt;code&gt;systemd-repart --dry-run&lt;/code&gt; against an image file,&lt;/li&gt;
&lt;li&gt;boot a disposable VM on a larger virtual disk,&lt;/li&gt;
&lt;li&gt;verify partition layout with &lt;code&gt;lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,PARTLABEL&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;verify filesystem growth with &lt;code&gt;findmnt /&lt;/code&gt; and &lt;code&gt;df -h /&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;only then promote the image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That workflow is boring, and boring is exactly what you want from storage automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification commands after first boot
&lt;/h2&gt;

&lt;p&gt;After a test boot, I would check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; NAME,SIZE,FSTYPE,TYPE,MOUNTPOINTS,PARTLABEL,PARTTYPE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE,FSTYPE,OPTIONS /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; systemd-repart.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those four commands usually tell you whether:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the partition exists,&lt;/li&gt;
&lt;li&gt;the expected filesystem exists,&lt;/li&gt;
&lt;li&gt;the mount options include the growth path you expected, and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-repart&lt;/code&gt; did what the layout files said.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;I like &lt;code&gt;systemd-repart&lt;/code&gt; because it replaces a pile of first-boot storage glue with something declarative and reviewable.&lt;/p&gt;

&lt;p&gt;If your Linux images are meant to land on disks of different sizes, this is one of the cleanest ways to keep the shipped image small while still letting the installed system take ownership of real capacity on first boot.&lt;/p&gt;

&lt;p&gt;Just keep one boundary in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-repart&lt;/code&gt; manages &lt;strong&gt;partition intent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-growfs&lt;/code&gt; or equivalent handles &lt;strong&gt;filesystem expansion&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get that split right, and the whole setup becomes much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-repart(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/systemd/systemd-repart.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd-repart.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repart.d(5)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/systemd/repart.d.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/repart.d.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd.io, &lt;em&gt;Safely Building Images&lt;/em&gt;: &lt;a href="https://systemd.io/BUILDING_IMAGES/" rel="noopener noreferrer"&gt;https://systemd.io/BUILDING_IMAGES/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discoverable Partitions Specification: &lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/" rel="noopener noreferrer"&gt;https://uapi-group.org/specifications/specs/discoverable_partitions_specification/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-growfs(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/systemd/systemd-growfs.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd-growfs.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Using setuid for Everything: Practical Linux File Capabilities with getcap, setcap, and systemd</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 25 Apr 2026 19:21:39 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-using-setuid-for-everything-practical-linux-file-capabilities-with-getcap-setcap-and-systemd-528b</link>
      <guid>https://dev.to/lyraalishaikh/stop-using-setuid-for-everything-practical-linux-file-capabilities-with-getcap-setcap-and-systemd-528b</guid>
      <description>&lt;h1&gt;
  
  
  Stop Using setuid for Everything: Practical Linux File Capabilities with getcap, setcap, and systemd
&lt;/h1&gt;

&lt;p&gt;A lot of Linux software does not actually need full root power. It needs one specific privilege.&lt;/p&gt;

&lt;p&gt;Maybe it only needs to bind to port 80. Maybe it needs raw sockets. Maybe it needs one network admin action during startup. Reaching for &lt;code&gt;sudo&lt;/code&gt;, &lt;code&gt;setuid&lt;/code&gt;, or a root-owned service for all of that is the old habit, not the best habit.&lt;/p&gt;

&lt;p&gt;Linux capabilities split root's all-or-nothing privilege model into smaller units. Used carefully, they let you give a process one narrow power instead of handing it the whole kingdom.&lt;/p&gt;

&lt;p&gt;This guide is a practical walkthrough for auditing, granting, and verifying capabilities on Linux, with examples you can adapt on Debian, Ubuntu, and similar distributions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why capabilities are worth using
&lt;/h2&gt;

&lt;p&gt;Traditional Unix privilege is blunt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;root bypasses normal permission checks&lt;/li&gt;
&lt;li&gt;non-root users do not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Linux capabilities break that into separate privileges like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CAP_NET_BIND_SERVICE&lt;/code&gt; to bind to ports below 1024&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CAP_NET_RAW&lt;/code&gt; to use raw and packet sockets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; for a huge pile of admin operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is important: some capabilities are narrow, but some are still extremely broad. &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; is famously overloaded, so treating it as "basically root" is often the safer mental model.&lt;/p&gt;

&lt;p&gt;The operational goal is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;avoid &lt;code&gt;setuid root&lt;/code&gt; when one small privilege will do&lt;/li&gt;
&lt;li&gt;avoid running long-lived services as root when a bounded capability is enough&lt;/li&gt;
&lt;li&gt;verify what you changed, instead of assuming it is safe&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, audit what is already privileged
&lt;/h2&gt;

&lt;p&gt;On Debian and Ubuntu, the &lt;code&gt;getcap&lt;/code&gt; and &lt;code&gt;setcap&lt;/code&gt; tools come from &lt;code&gt;libcap2-bin&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;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; libcap2-bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List files that already carry file capabilities:&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;sudo &lt;/span&gt;getcap &lt;span class="nt"&gt;-r&lt;/span&gt; / 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical setuid files are worth reviewing too:&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;sudo &lt;/span&gt;find / &lt;span class="nt"&gt;-xdev&lt;/span&gt; &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%M %u %g %p\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you two different privilege surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;executables with &lt;strong&gt;file capabilities&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;executables with the &lt;strong&gt;setuid bit&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are replacing an old setuid helper, this comparison is the right place to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safest beginner use case: binding to port 80 without running as root
&lt;/h2&gt;

&lt;p&gt;A classic example is a service that only needs to listen on port 80 or 443.&lt;/p&gt;

&lt;p&gt;The relevant capability is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CAP_NET_BIND_SERVICE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suppose your service binary lives at &lt;code&gt;/usr/local/bin/myapp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Grant only that capability:&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;sudo &lt;/span&gt;setcap &lt;span class="s1"&gt;'cap_net_bind_service=+ep'&lt;/span&gt; /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getcap /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/local/bin/myapp cap_net_bind_service=ep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run the service as a non-root user and still bind to port 80.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important warning: do not put capabilities on a shared interpreter
&lt;/h2&gt;

&lt;p&gt;This is a common mistake.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; do this on a general-purpose interpreter such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/bin/python3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/bin/node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/bin/bash&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you attach a file capability to a widely used interpreter, every script launched through that interpreter can inherit that privilege path. That is usually much broader than you intended.&lt;/p&gt;

&lt;p&gt;Better options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;put the capability on a dedicated compiled binary&lt;/li&gt;
&lt;li&gt;use a service manager such as systemd to grant the capability to one service&lt;/li&gt;
&lt;li&gt;front the app with a reverse proxy that already handles privileged ports&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prefer systemd for services you own
&lt;/h2&gt;

&lt;p&gt;For managed services, systemd is often cleaner than editing file metadata on the executable.&lt;/p&gt;

&lt;p&gt;Here is a minimal example for a service that should run as &lt;code&gt;myapp&lt;/code&gt;, bind to port 80, and get no extra network privileges beyond that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/myapp.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;My app&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/myapp&lt;/span&gt;
&lt;span class="py"&gt;AmbientCapabilities&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_NET_BIND_SERVICE&lt;/span&gt;
&lt;span class="py"&gt;CapabilityBoundingSet&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_NET_BIND_SERVICE&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it:&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;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this is nicer for long-lived services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;privilege is declared in the unit, not hidden on the file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CapabilityBoundingSet=&lt;/code&gt; limits what the service can ever retain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AmbientCapabilities=&lt;/code&gt; passes the needed capability to a non-root process&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NoNewPrivileges=true&lt;/code&gt; helps prevent gaining more privilege later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the resolved unit if you are debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;myapp.service
systemctl show myapp.service &lt;span class="nt"&gt;-p&lt;/span&gt; User &lt;span class="nt"&gt;-p&lt;/span&gt; Group &lt;span class="nt"&gt;-p&lt;/span&gt; AmbientCapabilities &lt;span class="nt"&gt;-p&lt;/span&gt; CapabilityBoundingSet &lt;span class="nt"&gt;-p&lt;/span&gt; NoNewPrivileges
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  File capabilities vs systemd capabilities
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;file capabilities&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you have one dedicated executable&lt;/li&gt;
&lt;li&gt;the privilege should travel with that executable&lt;/li&gt;
&lt;li&gt;the program may run outside systemd&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;strong&gt;systemd capability controls&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program is a service you manage&lt;/li&gt;
&lt;li&gt;you want the privilege policy next to the rest of the service definition&lt;/li&gt;
&lt;li&gt;you want a clean rollback by editing the unit rather than modifying executable metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My bias is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;for services, prefer systemd&lt;/li&gt;
&lt;li&gt;for one-off dedicated binaries, file capabilities can be fine&lt;/li&gt;
&lt;li&gt;for shared interpreters, avoid file capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Removing or changing a capability
&lt;/h2&gt;

&lt;p&gt;To remove file capabilities from an executable:&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;sudo &lt;/span&gt;setcap &lt;span class="nt"&gt;-r&lt;/span&gt; /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify removal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getcap /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing is printed, the file no longer has file capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second example: inspecting &lt;code&gt;ping&lt;/code&gt; is not enough anymore
&lt;/h2&gt;

&lt;p&gt;Older writeups often use &lt;code&gt;ping&lt;/code&gt; as the example for &lt;code&gt;CAP_NET_RAW&lt;/code&gt; or setuid. That is not reliable as a universal teaching shortcut now.&lt;/p&gt;

&lt;p&gt;Modern distributions vary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some ship &lt;code&gt;ping&lt;/code&gt; with file capabilities&lt;/li&gt;
&lt;li&gt;some historically used setuid&lt;/li&gt;
&lt;li&gt;some rely on kernel support for unprivileged ICMP echo sockets with &lt;code&gt;net.ipv4.ping_group_range&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if you are auditing a real host, inspect the local system rather than assuming what &lt;code&gt;/usr/bin/ping&lt;/code&gt; looks like.&lt;/p&gt;

&lt;p&gt;Useful checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getcap &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ping&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%A %U:%G %n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ping&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
sysctl net.ipv4.ping_group_range 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That small habit avoids a lot of copy-paste folklore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability names matter, and some are far riskier than they sound
&lt;/h2&gt;

&lt;p&gt;A few practical rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prefer the narrowest capability that solves the problem&lt;/li&gt;
&lt;li&gt;be suspicious of &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;treat capability changes like a security change, not a convenience tweak&lt;/li&gt;
&lt;li&gt;document why the capability exists&lt;/li&gt;
&lt;li&gt;test as the unprivileged service user, not only as root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a bad pattern:&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;sudo &lt;/span&gt;setcap &lt;span class="s1"&gt;'cap_sys_admin=+ep'&lt;/span&gt; /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of pattern you should look for first:&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;sudo &lt;/span&gt;setcap &lt;span class="s1"&gt;'cap_net_bind_service=+ep'&lt;/span&gt; /usr/local/bin/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A practical audit workflow
&lt;/h2&gt;

&lt;p&gt;When you want to replace broad privilege with something tighter, this sequence works well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;identify what the program actually needs to do&lt;/li&gt;
&lt;li&gt;map that to the smallest capability that matches&lt;/li&gt;
&lt;li&gt;prefer service-level controls if the app is systemd-managed&lt;/li&gt;
&lt;li&gt;verify the file or service configuration after the change&lt;/li&gt;
&lt;li&gt;run a real functional test as the target non-root user&lt;/li&gt;
&lt;li&gt;document the reason so the next admin does not "fix" it back to root&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example verification checklist:&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;# file metadata&lt;/span&gt;
getcap /usr/local/bin/myapp

&lt;span class="c"&gt;# service policy&lt;/span&gt;
systemctl show myapp.service &lt;span class="nt"&gt;-p&lt;/span&gt; AmbientCapabilities &lt;span class="nt"&gt;-p&lt;/span&gt; CapabilityBoundingSet &lt;span class="nt"&gt;-p&lt;/span&gt; NoNewPrivileges

&lt;span class="c"&gt;# listener really came up on a privileged port&lt;/span&gt;
ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; &lt;span class="s1"&gt;'( sport = :80 )'&lt;/span&gt;

&lt;span class="c"&gt;# service identity&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; user,group,comm,args &lt;span class="nt"&gt;-C&lt;/span&gt; myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When capabilities are the wrong tool
&lt;/h2&gt;

&lt;p&gt;Capabilities are not a magic replacement for every privileged workflow.&lt;/p&gt;

&lt;p&gt;They are often the wrong choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application still needs broad filesystem access that effectively requires root&lt;/li&gt;
&lt;li&gt;you are tempted to use &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the program is launched through a shared interpreter&lt;/li&gt;
&lt;li&gt;a reverse proxy, socket activation, or a small privileged helper would be cleaner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Least privilege is not just "fewer root shells". It is choosing the least dangerous mechanism that still keeps operations simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If a service only needs one narrow privilege, give it one narrow privilege.&lt;/p&gt;

&lt;p&gt;That is the real value of Linux capabilities. Not novelty, not cleverness, just a smaller blast radius and a setup you can actually explain during an audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Linux capabilities overview: &lt;a href="https://man7.org/linux/man-pages/man7/capabilities.7.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man7/capabilities.7.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setcap(8)&lt;/code&gt; manual: &lt;a href="https://man7.org/linux/man-pages/man8/setcap.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/setcap.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getcap(8)&lt;/code&gt; manual: &lt;a href="https://man7.org/linux/man-pages/man8/getcap.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/getcap.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd execution environment, including &lt;code&gt;AmbientCapabilities=&lt;/code&gt; and &lt;code&gt;CapabilityBoundingSet=&lt;/code&gt;: &lt;a href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/systemd.exec.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux ICMP and &lt;code&gt;ping_group_range&lt;/code&gt;: &lt;a href="https://man7.org/linux/man-pages/man7/icmp.7.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man7/icmp.7.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Keep Your Base OS Clean: Practical `systemd-sysext` for Linux Tools and Overrides</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 20 Apr 2026 11:39:13 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/keep-your-base-os-clean-practical-systemd-sysext-for-linux-tools-and-overrides-395n</link>
      <guid>https://dev.to/lyraalishaikh/keep-your-base-os-clean-practical-systemd-sysext-for-linux-tools-and-overrides-395n</guid>
      <description>&lt;h1&gt;
  
  
  Keep Your Base OS Clean: Practical &lt;code&gt;systemd-sysext&lt;/code&gt; for Linux Tools and Overrides
&lt;/h1&gt;

&lt;p&gt;I like keeping the base OS boring.&lt;/p&gt;

&lt;p&gt;That does not mean the machine has to stay limited. It means I want a clean line between the core system and the extra bits I only need sometimes, especially on hosts where &lt;code&gt;/usr&lt;/code&gt; is meant to stay stable.&lt;/p&gt;

&lt;p&gt;That is where &lt;code&gt;systemd-sysext&lt;/code&gt; gets interesting.&lt;/p&gt;

&lt;p&gt;It lets you merge additional files into &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; at runtime using overlayfs, without permanently modifying the host tree. Unmerge the extension, and those files disappear again. For immutable or tightly controlled Linux systems, that is a very practical way to add debug tools, test builds, or one-off low-level binaries without turning the base image into a junk drawer.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a safe, directory-based workflow you can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-sysext&lt;/code&gt; is good at
&lt;/h2&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-sysext&lt;/code&gt; documentation, system extension images are meant to extend &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; dynamically at runtime, and they are especially useful when the base OS image is read-only or intended to remain unchanged. The merge is read-only, and while active, the host's &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; also become read-only.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;systemd-sysext&lt;/code&gt; a good fit for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipping optional troubleshooting tools&lt;/li&gt;
&lt;li&gt;testing a newer build of a low-level binary&lt;/li&gt;
&lt;li&gt;layering in site-specific files on top of a controlled base image&lt;/li&gt;
&lt;li&gt;keeping the base OS reproducible while still allowing operational flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;em&gt;not&lt;/em&gt; a general-purpose package manager. There is no dependency solver here. The docs are pretty explicit about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;A few boundaries matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysext&lt;/code&gt; merges only &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;files inside &lt;code&gt;/etc&lt;/code&gt; and &lt;code&gt;/var&lt;/code&gt; in the extension are ignored by sysext&lt;/li&gt;
&lt;li&gt;it is additive by design, even though overlayfs technically allows replacement behavior&lt;/li&gt;
&lt;li&gt;it is not the right tool for shipping system services early in boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to deliver service units in an image with tighter isolation, &lt;code&gt;portablectl&lt;/code&gt; and portable services are the closer fit.&lt;/p&gt;

&lt;p&gt;If you want runtime config layering for &lt;code&gt;/etc&lt;/code&gt;, look at &lt;code&gt;systemd-confext&lt;/code&gt;, not sysext.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-duplication note
&lt;/h2&gt;

&lt;p&gt;Recent posts already covered &lt;code&gt;systemd-delta&lt;/code&gt;, &lt;code&gt;systemd-tmpfiles&lt;/code&gt;, socket activation, &lt;code&gt;systemd-oomd&lt;/code&gt;, and other systemd operations topics. I am intentionally taking a different angle here: runtime extension images for &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt;, not unit override auditing, cleanup policy, or service lifecycle tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Linux host with &lt;code&gt;systemd-sysext&lt;/code&gt; available&lt;/li&gt;
&lt;li&gt;root access for installation into system extension paths&lt;/li&gt;
&lt;li&gt;overlayfs support in the kernel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check whether the tool exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext &lt;span class="nt"&gt;--version&lt;/span&gt;
systemd-sysext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On many systems, extension images are searched in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For actual installed content, &lt;code&gt;/var/lib/extensions/&lt;/code&gt; is the normal place to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compatibility rule that trips people up
&lt;/h2&gt;

&lt;p&gt;Every sysext image needs an extension metadata file at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/lib/extension-release.d/extension-release.NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NAME&lt;/code&gt; must match the image or directory name.&lt;/p&gt;

&lt;p&gt;That file is checked against the host OS metadata. Per the man page, the extension's &lt;code&gt;ID=&lt;/code&gt; must match the host unless you deliberately set &lt;code&gt;_any&lt;/code&gt;. If &lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt; is present, it must match. Otherwise &lt;code&gt;VERSION_ID=&lt;/code&gt; is used as the compatibility check.&lt;/p&gt;

&lt;p&gt;For a directory named &lt;code&gt;debug-tools&lt;/code&gt;, the file must be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/lib/extension-release.d/extension-release.debug-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;debian&lt;/span&gt;
&lt;span class="py"&gt;VERSION_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;
&lt;span class="py"&gt;SYSEXT_SCOPE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;
&lt;span class="py"&gt;ARCHITECTURE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;x86-64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ID=&lt;/code&gt; should match your host OS family&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VERSION_ID=&lt;/code&gt; is the fallback compatibility gate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ARCHITECTURE=&lt;/code&gt; should match the host architecture when set&lt;/li&gt;
&lt;li&gt;do &lt;strong&gt;not&lt;/strong&gt; put &lt;code&gt;os-release&lt;/code&gt; in the extension's &lt;code&gt;/usr/lib&lt;/code&gt;, because that would shadow the host metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build a simple directory-based extension
&lt;/h2&gt;

&lt;p&gt;Let us create a tiny extension that drops one helper script into &lt;code&gt;/usr/local/bin&lt;/code&gt; and one documentation file into &lt;code&gt;/opt&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;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/usr/local/bin
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/usr/lib/extension-release.d
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/opt/debug-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the compatibility file:&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;sudo tee&lt;/span&gt; /var/lib/extensions/debug-tools/usr/lib/extension-release.d/extension-release.debug-tools &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
ID=debian
VERSION_ID=12
SYSEXT_SCOPE=system
ARCHITECTURE=x86-64
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a small helper script:&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;sudo tee&lt;/span&gt; /var/lib/extensions/debug-tools/usr/local/bin/hello-sysext &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/usr/bin/env bash
set -euo pipefail
printf 'hello from systemd-sysext&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 /var/lib/extensions/debug-tools/usr/local/bin/hello-sysext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add an optional file under &lt;code&gt;/opt&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;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/lib/extensions/debug-tools/opt/debug-tools/README.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
This file is provided by the debug-tools system extension.
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Activate it
&lt;/h2&gt;

&lt;p&gt;Refresh sysext state:&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;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the extension is accepted, you should now see the files through the live host tree:&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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; hello-sysext
hello-sysext
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /opt/debug-tools
&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/debug-tools/README.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see all recognized extensions, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Remove it cleanly
&lt;/h2&gt;

&lt;p&gt;To make the files disappear from the merged view:&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;sudo &lt;/span&gt;systemd-sysext unmerge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To bring them back:&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;sudo &lt;/span&gt;systemd-sysext merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To update after changing files inside the extension directory:&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;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;refresh&lt;/code&gt; flow is the one you will use most in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic use case: layering in a locally built binary
&lt;/h2&gt;

&lt;p&gt;One of the documented uses is to stage a newer build of a low-level component without rebuilding the whole base OS.&lt;/p&gt;

&lt;p&gt;For example, if you have a Makefile that supports &lt;code&gt;DESTDIR&lt;/code&gt;, you can install into the extension directory instead of the live root:&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;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/mytest
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;DESTDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/extensions/mytest make &lt;span class="nb"&gt;install
sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/mytest/usr/lib/extension-release.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/lib/extensions/mytest/usr/lib/extension-release.d/extension-release.mytest &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
ID=debian
VERSION_ID=12
SYSEXT_SCOPE=system
ARCHITECTURE=x86-64
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a reversible way to test files as if they were part of the base image, but without permanently mutating &lt;code&gt;/usr&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mask an extension without deleting it
&lt;/h2&gt;

&lt;p&gt;There is no classic enable or disable toggle per extension. Installed extensions are activated automatically at boot if &lt;code&gt;systemd-sysext.service&lt;/code&gt; is enabled.&lt;/p&gt;

&lt;p&gt;But the docs provide a neat masking trick: create an empty directory with the same name in &lt;code&gt;/etc/extensions/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&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;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/extensions/debug-tools
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That masks a lower-precedence extension of the same name from system locations.&lt;/p&gt;

&lt;p&gt;To unmask it:&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;sudo rmdir&lt;/span&gt; /etc/extensions/debug-tools
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;p&gt;If your extension does not appear, check these first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Name mismatch
&lt;/h3&gt;

&lt;p&gt;The directory name and &lt;code&gt;extension-release.NAME&lt;/code&gt; must match.&lt;/p&gt;

&lt;p&gt;For example, this is valid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;directory: &lt;code&gt;debug-tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;file: &lt;code&gt;extension-release.debug-tools&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. OS compatibility mismatch
&lt;/h3&gt;

&lt;p&gt;If the extension says &lt;code&gt;ID=ubuntu&lt;/code&gt; and the host is Debian, sysext should reject it.&lt;/p&gt;

&lt;p&gt;Likewise, &lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt; or &lt;code&gt;VERSION_ID=&lt;/code&gt; must line up with the host metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Wrong paths inside the extension
&lt;/h3&gt;

&lt;p&gt;Only &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; are merged by sysext.&lt;/p&gt;

&lt;p&gt;If you put content under &lt;code&gt;/etc/myapp&lt;/code&gt;, sysext will ignore it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. You forgot to refresh
&lt;/h3&gt;

&lt;p&gt;After adding or removing files in &lt;code&gt;/var/lib/extensions/...&lt;/code&gt;, run:&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;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. You expected writable merged paths
&lt;/h3&gt;

&lt;p&gt;While sysext is active, the merged &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; views are read-only.&lt;/p&gt;

&lt;p&gt;That is deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would use packages instead
&lt;/h2&gt;

&lt;p&gt;I would still prefer normal packages when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I want dependency management&lt;/li&gt;
&lt;li&gt;I want normal upgrade and removal tracking&lt;/li&gt;
&lt;li&gt;I am distributing software broadly to many mixed systems&lt;/li&gt;
&lt;li&gt;the host is not trying to keep &lt;code&gt;/usr&lt;/code&gt; controlled or reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;systemd-sysext&lt;/code&gt; shines when the host image is treated as a base artifact and you want optional or reversible layering on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  When portable services are the better tool
&lt;/h2&gt;

&lt;p&gt;This trips people up because both concepts involve image-based delivery.&lt;/p&gt;

&lt;p&gt;My rule of thumb is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;strong&gt;sysext&lt;/strong&gt; when you want extra files to appear in &lt;code&gt;/usr&lt;/code&gt; or &lt;code&gt;/opt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;portable services&lt;/strong&gt; when you want to ship services in an image and manage them as services, with service-level sandboxing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The systemd documentation explicitly calls out that difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I would not use &lt;code&gt;systemd-sysext&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;But on a host where the base OS should stay clean, predictable, and easy to reason about, it is a sharp tool. You get a reversible layer for binaries and support files, and the operational model stays simple: build the extension tree, add the compatibility metadata, then &lt;code&gt;refresh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a nice trade, especially when the alternative is "just copy it into &lt;code&gt;/usr/local&lt;/code&gt; and hope we remember later."&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;systemd-sysext man page: &lt;a href="https://manpages.debian.org/bookworm-backports/systemd/systemd-sysext.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm-backports/systemd/systemd-sysext.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Portable Services introduction: &lt;a href="https://systemd.io/PORTABLE_SERVICES/" rel="noopener noreferrer"&gt;https://systemd.io/PORTABLE_SERVICES/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;extension-release format reference: &lt;a href="https://www.freedesktop.org/software/systemd/man/extension-release.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/extension-release.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discoverable Partitions Specification: &lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/" rel="noopener noreferrer"&gt;https://uapi-group.org/specifications/specs/discoverable_partitions_specification/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Rebooting Linux Just in Case: Practical `needrestart` After APT Upgrades</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 19 Apr 2026 05:02:48 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-rebooting-linux-just-in-case-practical-needrestart-after-apt-upgrades-58j6</link>
      <guid>https://dev.to/lyraalishaikh/stop-rebooting-linux-just-in-case-practical-needrestart-after-apt-upgrades-58j6</guid>
      <description>&lt;p&gt;If you manage Debian or Ubuntu systems long enough, you eventually hit the same messy question after &lt;code&gt;apt upgrade&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;"Do I actually need to reboot this machine, or do I just need to restart a few services?"&lt;/p&gt;

&lt;p&gt;A lot of admins solve that uncertainty with habit: reboot everything. It works, but it is often unnecessary, and on production boxes it can be a sloppy answer to a more precise problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;needrestart&lt;/code&gt; is the tool built for that gap. It checks which running processes still use old libraries after package upgrades, can detect pending kernel upgrades, and integrates with APT through hooks.&lt;/p&gt;

&lt;p&gt;This guide shows a safe, practical workflow for using it without turning every patch cycle into an avoidable reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;needrestart&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;According to the Debian and Ubuntu man pages, &lt;code&gt;needrestart&lt;/code&gt; checks which daemons need to be restarted after library upgrades. It also supports checking for an obsolete kernel, and in batch mode it can produce machine-friendly output for scripting and monitoring.&lt;/p&gt;

&lt;p&gt;That distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some updates only require service restarts&lt;/li&gt;
&lt;li&gt;some updates leave user sessions or daemons mapped to old libraries&lt;/li&gt;
&lt;li&gt;kernel changes still require a reboot to boot into the new kernel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the question is not just "was there an update?" It is "what is still running the old code?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is different from &lt;code&gt;unattended-upgrades&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unattended-upgrades&lt;/code&gt; is the mechanism that installs approved updates automatically. Its own documentation says it logs activity to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/var/log/unattended-upgrades/unattended-upgrades.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/log/unattended-upgrades/unattended-upgrades-dpkg.log&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That tells you &lt;strong&gt;what got installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;needrestart&lt;/code&gt; tells you &lt;strong&gt;what still needs attention after installation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One subtle but important behavior from the &lt;code&gt;needrestart&lt;/code&gt; man page: if it is configured for interactive mode but runs in a non-interactive context such as &lt;code&gt;unattended-upgrades&lt;/code&gt;, it falls back to &lt;strong&gt;list-only&lt;/strong&gt; mode. That is a good default for automation, because it avoids surprise restarts during unattended patching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;

&lt;p&gt;On Debian or Ubuntu:&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;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;needrestart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;needrestart &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package is present but your normal patch workflow has never shown any &lt;code&gt;needrestart&lt;/code&gt; summary, it is still worth running manually once after an upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safest manual workflow
&lt;/h2&gt;

&lt;p&gt;After upgrading packages, run &lt;code&gt;needrestart&lt;/code&gt; in list-only mode first:&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;sudo &lt;/span&gt;apt upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-r&lt;/span&gt; l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-r l&lt;/code&gt; means list-only restart mode&lt;/li&gt;
&lt;li&gt;it reports what needs a restart without restarting anything&lt;/li&gt;
&lt;li&gt;it can also report whether the running kernel is older than the installed one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the mode I recommend first on servers, especially if you are patching over SSH or touching stateful workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: service restart instead of full reboot
&lt;/h2&gt;

&lt;p&gt;Imagine you upgraded OpenSSL or glibc on a host running Nginx, SSH, and a few app services.&lt;/p&gt;

&lt;p&gt;A cautious workflow looks like this:&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;sudo &lt;/span&gt;apt upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-r&lt;/span&gt; l
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-r&lt;/span&gt; l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why run it twice?&lt;/p&gt;

&lt;p&gt;Because the first pass tells you what is stale. After you restart the affected services, the second pass confirms whether you cleared the backlog or whether a reboot is still justified.&lt;/p&gt;

&lt;p&gt;You can also inspect service state directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status nginx &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
systemctl status myapp.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Batch mode for automation and monitoring
&lt;/h2&gt;

&lt;p&gt;One of &lt;code&gt;needrestart&lt;/code&gt;'s most useful features is batch mode:&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;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upstream batch-mode documentation shows output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEEDRESTART-VER: 2.1
NEEDRESTART-KCUR: 3.19.3-tl1+
NEEDRESTART-KEXP: 3.19.3-tl1+
NEEDRESTART-KSTA: 1
NEEDRESTART-SVC: systemd-journald.service
NEEDRESTART-SVC: systemd-machined.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few useful fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NEEDRESTART-SVC&lt;/code&gt; lists services that should be restarted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEEDRESTART-KCUR&lt;/code&gt; is the current kernel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEEDRESTART-KEXP&lt;/code&gt; is the expected kernel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEEDRESTART-KSTA&lt;/code&gt; is kernel status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upstream documents these kernel status values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt;: unknown or failed to detect&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt;: no pending upgrade&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt;: ABI-compatible upgrade pending&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3&lt;/code&gt;: version upgrade pending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes batch mode easy to wire into health checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  A small shell check for alerts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-b&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^NEEDRESTART-KSTA: [23]$'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Kernel reboot pending"&lt;/span&gt;
&lt;span class="k"&gt;fi

if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^NEEDRESTART-SVC:'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"One or more services need restart"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could run that from a systemd timer, a monitoring agent, or a post-upgrade audit script.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical reboot decision tree
&lt;/h2&gt;

&lt;p&gt;Here is the simplest policy that stays honest:&lt;/p&gt;

&lt;h3&gt;
  
  
  Reboot the host when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;needrestart&lt;/code&gt; shows a pending kernel upgrade&lt;/li&gt;
&lt;li&gt;you updated something that your own platform policy requires a reboot for&lt;/li&gt;
&lt;li&gt;you want a clean maintenance window reset after broad base-system changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prefer targeted service restarts when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;only specific daemons are using old libraries&lt;/li&gt;
&lt;li&gt;the host runs long-lived services you can restart one by one&lt;/li&gt;
&lt;li&gt;you want to avoid rebooting a production node unnecessarily&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Do a second verification pass when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;you restarted the listed services manually&lt;/li&gt;
&lt;li&gt;you are patching a critical host and want proof that stale processes are gone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second pass is the part many people skip, and it is where &lt;code&gt;needrestart&lt;/code&gt; earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it with unattended upgrades
&lt;/h2&gt;

&lt;p&gt;If you already use &lt;code&gt;unattended-upgrades&lt;/code&gt;, keep the responsibility split clean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;let &lt;code&gt;unattended-upgrades&lt;/code&gt; install packages&lt;/li&gt;
&lt;li&gt;review its logs if needed&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;needrestart&lt;/code&gt; output to decide between service restarts and a reboot&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For hosts where you do not want the APT hook to run &lt;code&gt;needrestart&lt;/code&gt; automatically, the man page documents &lt;code&gt;NEEDRESTART_SUSPEND&lt;/code&gt; for suppressing the hook in an &lt;code&gt;apt-get&lt;/code&gt; context.&lt;/p&gt;

&lt;p&gt;Example:&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;sudo &lt;/span&gt;&lt;span class="nv"&gt;NEEDRESTART_SUSPEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 apt-get upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-r&lt;/span&gt; l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a fully explicit post-upgrade review step.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny post-upgrade helper script
&lt;/h2&gt;

&lt;p&gt;If you want a repeatable operator workflow, save this as &lt;code&gt;/usr/local/sbin/post-apt-restart-check&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;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;sudo &lt;/span&gt;needrestart &lt;span class="nt"&gt;-r&lt;/span&gt; l &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"If services are listed, restart them selectively with:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  sudo systemctl restart &amp;lt;service&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Then verify again with:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  sudo needrestart -r l"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&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;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 post-apt-restart-check /usr/local/sbin/post-apt-restart-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your patch routine becomes:&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;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/local/sbin/post-apt-restart-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is simple, but it turns post-upgrade guesswork into an explicit checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to assume
&lt;/h2&gt;

&lt;p&gt;A few guardrails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;needrestart&lt;/code&gt; helps identify stale daemons and pending kernel upgrades, but it is not a substitute for application-specific maintenance knowledge.&lt;/li&gt;
&lt;li&gt;Restarting a service may still need coordination if the app has connection draining, clustering, or session-state concerns.&lt;/li&gt;
&lt;li&gt;A clean &lt;code&gt;needrestart -r l&lt;/code&gt; result after service restarts is strong evidence, but your own change policy still wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: use the tool to reduce blind reboots, not to skip judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;If your current post-update policy is "reboot because maybe," &lt;code&gt;needrestart&lt;/code&gt; gives you a much sharper answer.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;-r l&lt;/code&gt; first, restart only what is actually stale, rerun the check, and reserve full reboots for when the kernel or your own operations policy genuinely requires them.&lt;/p&gt;

&lt;p&gt;That is a better patching habit, and a calmer one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian man page, &lt;code&gt;needrestart(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/needrestart/needrestart.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/needrestart/needrestart.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ubuntu man page, &lt;code&gt;needrestart(1)&lt;/code&gt;: &lt;a href="https://manpages.ubuntu.com/manpages/jammy/man1/needrestart.1.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/jammy/man1/needrestart.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Upstream &lt;code&gt;needrestart&lt;/code&gt; repository: &lt;a href="https://github.com/liske/needrestart" rel="noopener noreferrer"&gt;https://github.com/liske/needrestart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Upstream batch-mode documentation: &lt;a href="https://raw.githubusercontent.com/liske/needrestart/master/README.batch.md" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/liske/needrestart/master/README.batch.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian package metadata for &lt;code&gt;needrestart&lt;/code&gt;: &lt;a href="https://packages.debian.org/bookworm/needrestart" rel="noopener noreferrer"&gt;https://packages.debian.org/bookworm/needrestart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian man page, &lt;code&gt;unattended-upgrade(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/unattended-upgrades/unattended-upgrade.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/unattended-upgrades/unattended-upgrade.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ubuntu man page, &lt;code&gt;unattended-upgrade(8)&lt;/code&gt;: &lt;a href="https://manpages.ubuntu.com/manpages/jammy/man8/unattended-upgrade.8.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/jammy/man8/unattended-upgrade.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>automation</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
