<?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.us-east-2.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 Putting Secrets in Environment Variables: Practical systemd-creds on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:02:13 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-putting-secrets-in-environment-variables-practical-systemd-creds-on-linux-4gfi</link>
      <guid>https://dev.to/lyraalishaikh/stop-putting-secrets-in-environment-variables-practical-systemd-creds-on-linux-4gfi</guid>
      <description>&lt;p&gt;Passwords in &lt;code&gt;Environment=&lt;/code&gt; lines, API tokens in world-readable unit drop-ins, and &lt;code&gt;EnvironmentFile=/etc/myapp.env&lt;/code&gt; that every process on the host can &lt;code&gt;cat&lt;/code&gt; once it gets a shell — that pattern still shows up in homelab and production units alike.&lt;/p&gt;

&lt;p&gt;systemd has a better primitive: &lt;strong&gt;credentials&lt;/strong&gt;. They are limited-size binary or text blobs that the service manager loads at activation time, decrypts if needed, places in a private directory, and exposes to the service via &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;. The operator tool is &lt;code&gt;systemd-creds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and behavior come from &lt;code&gt;systemd-creds(1)&lt;/code&gt;, the &lt;strong&gt;Credentials&lt;/strong&gt; section of &lt;code&gt;systemd.exec(5)&lt;/code&gt;, and the upstream &lt;a href="https://systemd.io/CREDENTIALS/" rel="noopener noreferrer"&gt;System and Service Credentials&lt;/a&gt; document. Examples target systemd 250+ (encryption tooling); user-scoped encrypt/decrypt needs 256+. The host used for command verification here runs systemd 257.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use an env var?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Environment=SECRET=…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unit files are world-readable on disk and via D-Bus; children inherit the env&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EnvironmentFile=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same visibility problem; easy to leave mode &lt;code&gt;0644&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain file under &lt;code&gt;/etc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Readable by any process that can open it; not scoped to the unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOPS/age in Git&lt;/td&gt;
&lt;td&gt;Great for repo secrets; still need a &lt;strong&gt;runtime&lt;/strong&gt; hand-off into the service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clevis/Tang / LUKS&lt;/td&gt;
&lt;td&gt;Disk unlock, not per-service secret injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Credentials fix the runtime side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acquired at activation, released on deactivation, immutable while the service runs&lt;/li&gt;
&lt;li&gt;Files under &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;, mode-restricted to the service UID (and root)&lt;/li&gt;
&lt;li&gt;Optionally AES-256-GCM encrypted/authenticated at rest (TPM2 and/or host secret)&lt;/li&gt;
&lt;li&gt;Preferable to env for secrets; binary-safe; ~1 MB accumulated size limit per unit&lt;/li&gt;
&lt;li&gt;Work cleanly with &lt;code&gt;RootDirectory=&lt;/code&gt; / &lt;code&gt;RootImage=&lt;/code&gt; / portable services (host path not required inside the image)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; systemd-creds
systemd-creds &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# encrypt/decrypt/list/cat/setup: systemd 250+&lt;/span&gt;
&lt;span class="c"&gt;# --user / --uid= encrypt: 256+&lt;/span&gt;

&lt;span class="c"&gt;# Optional: see whether TPM2 is usable for credential protection&lt;/span&gt;
systemd-creds has-tpm2 &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;You need root (or equivalent) for host-key setup and for system-unit examples. Transient labs use &lt;code&gt;systemd-run&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five unit settings (and when to use each)
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;systemd.exec(5)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Sensitive?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadCredential=ID[:PATH]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File, AF_UNIX socket, or relative search path&lt;/td&gt;
&lt;td&gt;Path should be protected; data stays plaintext on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadCredentialEncrypted=ID[:PATH]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypted credential file/socket&lt;/td&gt;
&lt;td&gt;Yes — decrypt at activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SetCredential=ID:VALUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Literal in the unit file&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; for secrets (public keys, IDs only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SetCredentialEncrypted=ID:VALUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypted blob literal in the unit&lt;/td&gt;
&lt;td&gt;Yes — safe to paste ciphertext into drop-ins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImportCredential=GLOB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Propagate system credentials (or search stores) by name/glob&lt;/td&gt;
&lt;td&gt;Depends on how the system got them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Search paths for relative sources (also used by &lt;code&gt;ImportCredential=&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain: &lt;code&gt;/etc/credstore/&lt;/code&gt;, &lt;code&gt;/run/credstore/&lt;/code&gt;, &lt;code&gt;/usr/lib/credstore/&lt;/code&gt; (plus credentials already passed into the system)&lt;/li&gt;
&lt;li&gt;Encrypted: &lt;code&gt;/etc/credstore.encrypted/&lt;/code&gt;, &lt;code&gt;/run/credstore.encrypted/&lt;/code&gt;, &lt;code&gt;/usr/lib/credstore.encrypted/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Per-user manager: &lt;code&gt;$XDG_CONFIG_HOME/credstore/&lt;/code&gt;, &lt;code&gt;$XDG_RUNTIME_DIR/credstore/&lt;/code&gt;, &lt;code&gt;$HOME/.local/lib/credstore/&lt;/code&gt; (and &lt;code&gt;.encrypted&lt;/code&gt; counterparts)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$CREDENTIALS_DIRECTORY/&amp;lt;ID&amp;gt;   # primary interface
%d/&amp;lt;ID&amp;gt;                       # unit-file specifier for Environment=
${CREDENTIALS_DIRECTORY}/&amp;lt;ID&amp;gt; # ExecStart= interpolation
/run/credentials/&amp;lt;unit&amp;gt;       # system services only — prefer $CREDENTIALS_DIRECTORY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 1 — Plain LoadCredential with a transient unit
&lt;/h2&gt;

&lt;p&gt;Prove the plumbing before encryption:&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;# As root&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'hello-from-disk'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/demo-plain.txt
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /root/demo-plain.txt

systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  systemd-creds &lt;span class="nb"&gt;cat &lt;/span&gt;demo
&lt;span class="c"&gt;# → hello-from-disk&lt;/span&gt;

systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  systemd-creds list
&lt;span class="c"&gt;# Shows name, size, and security state (secure/weak/insecure)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemd-creds list&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt; read &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; in the &lt;strong&gt;current&lt;/strong&gt; execution context — that is why the lab runs them &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;systemd-run&lt;/code&gt; with &lt;code&gt;LoadCredential=&lt;/code&gt; attached.&lt;/p&gt;

&lt;p&gt;Reference the path from a shell-style &lt;code&gt;ExecStart&lt;/code&gt; without putting the secret in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'wc -c &amp;lt; "$CREDENTIALS_DIRECTORY/demo"; sha256sum "$CREDENTIALS_DIRECTORY/demo"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Encrypt with the host key
&lt;/h2&gt;

&lt;p&gt;Default &lt;code&gt;encrypt&lt;/code&gt; picks a key mode automatically: TPM2 when present and not in a container, host secret when &lt;code&gt;/var/lib/systemd/&lt;/code&gt; is persistent media, and &lt;strong&gt;both&lt;/strong&gt; when both apply (&lt;code&gt;host+tpm2&lt;/code&gt;). On VMs or hosts without a usable TPM, force the host key:&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;# Ensures /var/lib/systemd/credential.secret exists (also done implicitly on encrypt)&lt;/span&gt;
systemd-creds setup

&lt;span class="c"&gt;# Encrypt stdin → file. Output basename becomes the embedded credential name.&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s3cr3t-db-password'&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host - /etc/credstore.encrypted/db-password.cred
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/credstore.encrypted/db-password.cred
&lt;span class="c"&gt;# Directory may need creating first:&lt;/span&gt;
&lt;span class="c"&gt;# mkdir -p /etc/credstore.encrypted &amp;amp;&amp;amp; chmod 700 /etc/credstore.encrypted&lt;/span&gt;

&lt;span class="c"&gt;# Round-trip check (name must match the embedded name = filename by default)&lt;/span&gt;
systemd-creds decrypt /etc/credstore.encrypted/db-password.cred
&lt;span class="c"&gt;# → s3cr3t-db-password&lt;/span&gt;

&lt;span class="c"&gt;# Wipe any leftover plaintext you used during creation&lt;/span&gt;
&lt;span class="nb"&gt;shred&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; /root/demo-plain.txt 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;Key modes from &lt;code&gt;systemd-creds(1)&lt;/code&gt; (&lt;code&gt;--with-key=&lt;/code&gt; / &lt;code&gt;-H&lt;/code&gt; / &lt;code&gt;-T&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Binds decryption to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;host&lt;/code&gt; (&lt;code&gt;-H&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/lib/systemd/credential.secret&lt;/code&gt; (root-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tpm2&lt;/code&gt; (&lt;code&gt;-T&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Local TPM2 only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;host+tpm2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both (typical default when both available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default selection logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto-initrd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TPM2 if present, else null-style fallback — for initrd/generator use where &lt;code&gt;/var&lt;/code&gt; is not mounted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed empty key — &lt;strong&gt;no&lt;/strong&gt; confidentiality/authenticity; restricted acceptance on Secure Boot + TPM systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Encryption algorithm: AES-256-GCM, keyed from a SHA-256 derivation of the selected secret(s). Ciphertext is Base64.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initrd / early-boot note:&lt;/strong&gt; if a credential must decrypt before &lt;code&gt;/var&lt;/code&gt; is available, do &lt;strong&gt;not&lt;/strong&gt; bind it to the host secret. Use &lt;code&gt;--with-key=tpm2&lt;/code&gt; or &lt;code&gt;auto-initrd&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — LoadCredentialEncrypted in a real unit drop-in
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example consumer: a oneshot that only prints that the secret length is correct&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/etc/systemd/system/cred-demo.service &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;'
[Unit]
Description=Credential demo service

[Service]
Type=oneshot
# Relative path → searched under credstore.encrypted (and friends)
LoadCredentialEncrypted=db-password
ExecStart=/usr/bin/bash -c 'test -n "&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS_DIRECTORY&lt;/span&gt;&lt;span class="sh"&gt;"; install -m 0400 "&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS_DIRECTORY&lt;/span&gt;&lt;span class="sh"&gt;/db-password" /run/cred-demo-check; wc -c /run/cred-demo-check; rm -f /run/cred-demo-check'
# Optional hardening that also implies PrivateMounts= on many distros:
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl start cred-demo.service
systemctl status cred-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; cred-demo.service &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit path form (same effect):&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;LoadCredentialEncrypted&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;db-password:/etc/credstore.encrypted/db-password.cred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed decrypt/authenticate for &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; / &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt; &lt;strong&gt;fails the unit&lt;/strong&gt;. Failed decrypt for something only pulled via &lt;code&gt;ImportCredential=&lt;/code&gt; is skipped with a warning (credential absent).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Paste ciphertext into the unit with SetCredentialEncrypted
&lt;/h2&gt;

&lt;p&gt;When you want the secret &lt;strong&gt;in&lt;/strong&gt; the drop-in (no separate &lt;code&gt;.cred&lt;/code&gt; file), generate a pasteable line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-ask-password &lt;span class="nt"&gt;-n&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql-password &lt;span class="nt"&gt;-p&lt;/span&gt; - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-p&lt;/code&gt; / &lt;code&gt;--pretty&lt;/code&gt; prints a &lt;code&gt;SetCredentialEncrypted=mysql-password: \&lt;/code&gt; block. Pipe it into a 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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/myapp.service.d
systemd-ask-password &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[Service]'&lt;/span&gt;
  systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql-password &lt;span class="nt"&gt;-p&lt;/span&gt; - -
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/etc/systemd/system/myapp.service.d/50-password.conf
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/systemd/system/myapp.service.d/50-password.conf
systemctl daemon-reload
systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit file remains world-readable in the usual case; the &lt;strong&gt;ciphertext&lt;/strong&gt; is not useful without the host secret and/or TPM. That is the point of &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt; versus plaintext &lt;code&gt;SetCredential=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Name embedding: the credential name is stored inside the encrypted blob. Renaming &lt;code&gt;mysql-password.cred&lt;/code&gt; to &lt;code&gt;other.cred&lt;/code&gt; makes decrypt fail unless you override with &lt;code&gt;--name=&lt;/code&gt;. Empty &lt;code&gt;--name=&lt;/code&gt; disables the check (rarely what you want).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Wire apps that do not know $CREDENTIALS_DIRECTORY yet
&lt;/h2&gt;

&lt;p&gt;Many daemons want a path on the CLI or a single env var pointing at a file — not an env var containing the secret itself.&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;[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;LoadCredentialEncrypted&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;api-token&lt;/span&gt;
&lt;span class="c"&gt;# Path-only env (the secret stays in the file; children do not inherit the bytes as env)
&lt;/span&gt;&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;API_TOKEN_FILE=%d/api-token&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 --token-file ${CREDENTIALS_DIRECTORY}/api-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;%d&lt;/code&gt; expands to the credentials directory in unit settings; &lt;code&gt;${CREDENTIALS_DIRECTORY}&lt;/code&gt; works in &lt;code&gt;Exec*=&lt;/code&gt; command lines. Prefer these over copying secrets into &lt;code&gt;Environment=SECRET=…&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For software that insists on reading &lt;code&gt;/run/credentials/myapp.service/api-token&lt;/code&gt;, that path works for &lt;strong&gt;system&lt;/strong&gt; units, but &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; is the portable interface (also correct for &lt;code&gt;systemd --user&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Propagate system credentials with ImportCredential
&lt;/h2&gt;

&lt;p&gt;Container managers, hypervisors, and cloud IMDS can pass credentials into PID 1. Enumerate them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-creds &lt;span class="nt"&gt;--system&lt;/span&gt; list
systemd-creds &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;some-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples of ingress (from the credentials documentation):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-nspawn --set-credential=name:value&lt;/code&gt; / &lt;code&gt;--load-credential=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;QEMU SMBIOS type 11: &lt;code&gt;io.systemd.credential:name=value&lt;/code&gt; or &lt;code&gt;io.systemd.credential.binary:name=&amp;lt;base64&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Kernel cmdline: &lt;code&gt;systemd.set_credential=&lt;/code&gt; / &lt;code&gt;systemd.set_credential_binary=&lt;/code&gt; (visible via &lt;code&gt;/proc/cmdline&lt;/code&gt; — &lt;strong&gt;not&lt;/strong&gt; for secrets)&lt;/li&gt;
&lt;li&gt;ESP credentials via &lt;code&gt;systemd-stub&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/run/credentials/@initrd/&lt;/code&gt; import on initrd → host transition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consume in a 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="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ImportCredential&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mycred&lt;/span&gt;
&lt;span class="c"&gt;# or rename:
# ImportCredential=my.original.cred:my.renamed.cred
# or glob:
# ImportCredential=app.*
&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/systemd-creds cat mycred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well-known system credentials (provisioning, not day-2 app secrets) include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;passwd.hashed-password.&amp;lt;user&amp;gt;&lt;/code&gt;, &lt;code&gt;passwd.plaintext-password.&amp;lt;user&amp;gt;&lt;/code&gt;, &lt;code&gt;passwd.shell.&amp;lt;user&amp;gt;&lt;/code&gt; → &lt;code&gt;systemd-sysusers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firstboot.locale&lt;/code&gt;, &lt;code&gt;firstboot.keymap&lt;/code&gt;, &lt;code&gt;firstboot.timezone&lt;/code&gt;, … → &lt;code&gt;systemd-firstboot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmpfiles.extra&lt;/code&gt; → tmpfiles&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmm.notify_socket&lt;/code&gt; → PID 1 READY notification (e.g. &lt;code&gt;vsock:CID:PORT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh.authorized_keys.root&lt;/code&gt; (IMDS import path)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;code&gt;systemd.system-credentials(7)&lt;/code&gt; for the full inventory. Conditional start: &lt;code&gt;ConditionCredential=&lt;/code&gt; / &lt;code&gt;AssertCredential=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  User services (systemd 256+)
&lt;/h2&gt;

&lt;p&gt;Encrypt for the per-user manager so the key material incorporates UID, username, and &lt;code&gt;machine-id&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;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'user-secret'&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;self - ~/credstore.encrypted/user-secret.cred
&lt;span class="c"&gt;# Load from a user unit with LoadCredentialEncrypted=user-secret&lt;/span&gt;
&lt;span class="c"&gt;# and store under ~/.config/credstore.encrypted/ or the paths listed above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; ciphertext with &lt;code&gt;systemd-creds encrypt&lt;/code&gt; (&lt;code&gt;--with-key=host&lt;/code&gt; in labs without TPM; default &lt;code&gt;auto&lt;/code&gt; on real hardware).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; under &lt;code&gt;/etc/credstore.encrypted/&lt;/code&gt; (mode &lt;code&gt;0700&lt;/code&gt; dir, &lt;code&gt;0600&lt;/code&gt; files) or embed with &lt;code&gt;-p&lt;/code&gt; → &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declare&lt;/strong&gt; &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; / &lt;code&gt;ImportCredential=&lt;/code&gt; on the unit; avoid plaintext &lt;code&gt;Environment=&lt;/code&gt; for secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consume&lt;/strong&gt; via &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; or &lt;code&gt;%d&lt;/code&gt; path references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox&lt;/strong&gt; with &lt;code&gt;ProtectSystem=&lt;/code&gt;, &lt;code&gt;PrivateTmp=&lt;/code&gt;, &lt;code&gt;ProtectHome=&lt;/code&gt;, or at least &lt;code&gt;PrivateMounts=&lt;/code&gt; so other units cannot see the credentials mount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; by re-encrypting and restarting the unit; ciphertext is immutable for the life of an activation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup&lt;/strong&gt; &lt;code&gt;/var/lib/systemd/credential.secret&lt;/code&gt; with the same care as disk-encryption keys if you rely on host binding — losing it bricks host-bound credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry&lt;/strong&gt; (optional): &lt;code&gt;systemd-creds encrypt --not-after=…&lt;/code&gt; embeds a hard fail-after timestamp checked at decrypt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Priority when the same ID is configured multiple ways (from &lt;code&gt;systemd.exec(5)&lt;/code&gt;): &lt;code&gt;LoadCredential*&lt;/code&gt; / &lt;code&gt;ImportCredential=&lt;/code&gt; win over &lt;code&gt;SetCredential*&lt;/code&gt;. &lt;code&gt;SetCredential=&lt;/code&gt; is a useful &lt;strong&gt;default&lt;/strong&gt; that is overridden when a real credential appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries (what this article is not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; SOPS + age in Git (repo encryption / GitOps). Credentials are the &lt;strong&gt;node-local runtime&lt;/strong&gt; injection layer after deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; Clevis + Tang NBDE or &lt;code&gt;systemd-cryptenroll&lt;/code&gt; (volume unlock at boot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;systemd-homed&lt;/code&gt; per-user home encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a full replacement for a corporate secret manager — but an excellent native fit for machine-local service secrets on systemd hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;varlinkctl&lt;/code&gt; / D-Bus service APIs, linger/user managers, or image tooling from recent posts — only the credential object model and &lt;code&gt;systemd-creds&lt;/code&gt; CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop cred-demo.service 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/cred-demo.service
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/myapp.service.d/50-password.conf
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/credstore.encrypted/db-password.cred
systemctl daemon-reload
&lt;span class="c"&gt;# Keep /var/lib/systemd/credential.secret unless you intend to invalidate host-bound creds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd-creds.1.en.html" rel="noopener noreferrer"&gt;systemd-creds(1)&lt;/a&gt; — list, cat, setup, encrypt, decrypt, key modes, examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd.exec.5.en.html" rel="noopener noreferrer"&gt;systemd.exec(5) — Credentials&lt;/a&gt; — &lt;code&gt;LoadCredential*&lt;/code&gt;, &lt;code&gt;SetCredential*&lt;/code&gt;, &lt;code&gt;ImportCredential=&lt;/code&gt;, &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;, size limit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://systemd.io/CREDENTIALS/" rel="noopener noreferrer"&gt;System and Service Credentials&lt;/a&gt; — design goals, encryption, ingress paths, well-known credentials, search paths&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd.system-credentials.7.en.html" rel="noopener noreferrer"&gt;systemd.system-credentials(7)&lt;/a&gt; — well-known system credential names&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/systemd-nspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-nspawn(1)&lt;/a&gt; — &lt;code&gt;--set-credential=&lt;/code&gt; / &lt;code&gt;--load-credential=&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop putting long-lived secrets in &lt;code&gt;Environment=&lt;/code&gt; and world-readable env files. Encrypt once with &lt;code&gt;systemd-creds&lt;/code&gt;, load with &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; or &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt;, and let the service read a file from &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; like any other path-based secret API — with activation-time decrypt and per-unit isolation included.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Guessing systemd Service APIs: Practical varlinkctl on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 29 Sep 2026 05:02:08 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-systemd-service-apis-practical-varlinkctl-on-linux-59n</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-systemd-service-apis-practical-varlinkctl-on-linux-59n</guid>
      <description>&lt;p&gt;You need one clean answer from a local service: resolve a name, describe the host, extend a PCR, or talk to a custom tool that already speaks JSON on stdin/stdout. The usual options are brittle: scrape &lt;code&gt;systemctl&lt;/code&gt; text, write a one-off D-Bus client, or invent yet another Unix-socket protocol.&lt;/p&gt;

&lt;p&gt;Modern systemd components expose &lt;strong&gt;Varlink&lt;/strong&gt; interfaces instead. &lt;code&gt;varlinkctl&lt;/code&gt; is the operator-facing client for those interfaces: discover what a service implements, print the interface IDL, call methods with JSON arguments, stream multi-reply methods, tunnel calls over SSH, and even wrap a sandboxed command as a Varlink service.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and behavior come from &lt;code&gt;varlinkctl(1)&lt;/code&gt;, the UAPI.20 Varlink IPC specification, and the Debian/unstable systemd man pages for the services used in the labs (notably &lt;code&gt;systemd-resolved&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What Varlink is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Varlink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON method calls over a stream (typically &lt;code&gt;AF_UNIX&lt;/code&gt;), with a typed interface definition language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UAPI.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The UAPI Group spec consolidating protocol + transport bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;varlinkctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CLI to introspect and invoke Varlink services (systemd 255+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt; / D-Bus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parallel IPC stack; still widely used, different wire format and tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemctl&lt;/code&gt; text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Human UI — fine for shells, awkward as a stable machine API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Varlink messages are plain JSON objects. On stream transports each message is NUL-terminated. Services describe themselves at runtime, so clients can list interfaces and fetch the IDL without a separate schema package.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need to replace every D-Bus workflow. Use &lt;code&gt;varlinkctl&lt;/code&gt; when the component already speaks Varlink (resolved, hostnamed, many systemd helpers) or when you want a JSON-friendly, socket-activated service of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; varlinkctl
varlinkctl &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Added in systemd 255; several commands below need 257–262 features.&lt;/span&gt;

&lt;span class="c"&gt;# Labs that call resolved need the stub/service running:&lt;/span&gt;
systemctl is-active systemd-resolved.service
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/systemd/resolve/io.systemd.Resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Privileged examples (PCR extend, some hostnamed paths) need root or an authorized policy. DNS resolve calls as a normal user usually work when resolved is active and the socket is accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Address forms you will actually use
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;varlinkctl&lt;/code&gt; accepts several service address syntaxes (from &lt;code&gt;varlinkctl(1)&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;unix:/run/path.sock&lt;/code&gt; or &lt;code&gt;/run/path.sock&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Connect to an &lt;code&gt;AF_UNIX&lt;/code&gt; stream socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@name&lt;/code&gt; after &lt;code&gt;unix:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Abstract-namespace socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exec:/usr/lib/systemd/tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fork the binary and speak Varlink on the passed socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh-unix:host:/run/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenSSH ≥ 9.4 path to a remote &lt;code&gt;AF_UNIX&lt;/code&gt; socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh-exec:host:cmdline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a remote command and speak Varlink on its stdio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relative &lt;code&gt;./socket&lt;/code&gt; or &lt;code&gt;./binary&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Local path forms (must start with &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;./&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Convenience: a bare absolute socket path or executable path is enough when the target is local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inventory a live service (resolved)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# General metadata&lt;/span&gt;
varlinkctl info /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Interface names only&lt;/span&gt;
varlinkctl list-interfaces /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Method names (list-methods added in 257)&lt;/span&gt;
varlinkctl list-methods /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Full IDL for the primary interface&lt;/span&gt;
varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical &lt;code&gt;info&lt;/code&gt; fields include vendor, product, version, URL, and the interface list (&lt;code&gt;io.systemd&lt;/code&gt;, &lt;code&gt;io.systemd.Resolve&lt;/code&gt;, &lt;code&gt;org.varlink.service&lt;/code&gt;, …).&lt;/p&gt;

&lt;p&gt;Pretty JSON for scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;varlinkctl info /run/systemd/resolve/io.systemd.Resolve &lt;span class="nt"&gt;-j&lt;/span&gt;
varlinkctl list-methods /run/systemd/resolve/io.systemd.Resolve &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-j&lt;/code&gt; is “pretty when interactive, short when piped”; &lt;code&gt;--json=pretty|short&lt;/code&gt; forces the mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Call a method with JSON arguments
&lt;/h2&gt;

&lt;p&gt;Resolve a hostname through resolved’s &lt;code&gt;ResolveHostname&lt;/code&gt; method (example shape from &lt;code&gt;varlinkctl(1)&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;varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  /run/systemd/resolve/io.systemd.Resolve &lt;span class="se"&gt;\&lt;/span&gt;
  io.systemd.Resolve.ResolveHostname &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"name":"systemd.io","family":2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that prevent foot-guns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method names are &lt;strong&gt;fully qualified&lt;/strong&gt;: &lt;code&gt;interface.Method&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Parameters are a &lt;strong&gt;JSON object&lt;/strong&gt;. Use &lt;code&gt;{}&lt;/code&gt; for empty input.&lt;/li&gt;
&lt;li&gt;If you omit the arguments parameter, &lt;code&gt;varlinkctl&lt;/code&gt; reads JSON from STDIN.&lt;/li&gt;
&lt;li&gt;Replies are JSON objects on STDOUT — pipe to &lt;code&gt;jq&lt;/code&gt; when you want fields only.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  /run/systemd/resolve/io.systemd.Resolve &lt;span class="se"&gt;\&lt;/span&gt;
  io.systemd.Resolve.ResolveHostname &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"name":"systemd.io","family":2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.addresses[]? | "\(.family) \(.address)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;family: 2&lt;/code&gt; is &lt;code&gt;AF_INET&lt;/code&gt; in the usual Linux numbering; adjust if you want IPv6 (&lt;code&gt;10&lt;/code&gt; / &lt;code&gt;AF_INET6&lt;/code&gt;) or leave the field out when the interface allows defaults (check the IDL from &lt;code&gt;introspect&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Multi-reply methods, collection, and oneway
&lt;/h2&gt;

&lt;p&gt;Some methods stream updates or enumerate objects. Flags from &lt;code&gt;varlinkctl(1)&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;# Expect a sequence of replies (JSON-SEQ). Default call timeout is still 45s.&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--more&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"...":"..."}'&lt;/span&gt;

&lt;span class="c"&gt;# Same idea, but keep listening (timeout disabled) — shortcut -E&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;-E&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Gather every reply into one JSON array&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--collect&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Fire-and-forget (no reply expected)&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--oneway&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"...":"..."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeout control:&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;# Default 45s; disable for long subscriptions&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--more&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infinity ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Treat a specific Varlink error as success (257+)&lt;/span&gt;
varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--graceful&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;org.varlink.service.InvalidParameter &lt;span class="se"&gt;\&lt;/span&gt;
  ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"experimental":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--more&lt;/code&gt; with a sane timeout for finite enumerations; use &lt;code&gt;-E&lt;/code&gt; / &lt;code&gt;--timeout=infinity&lt;/code&gt; only for true subscriptions so a stuck peer cannot hang a cron job forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Exec targets and helper binaries
&lt;/h2&gt;

&lt;p&gt;Not every Varlink peer is a long-running daemon socket. Some tools speak Varlink when executed. The man page demonstrates &lt;code&gt;systemd-pcrextend&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;# Inspect the executable as a Varlink service (requires privileges for real PCR ops)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;varlinkctl info /usr/lib/systemd/systemd-pcrextend
&lt;span class="nb"&gt;sudo &lt;/span&gt;varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend

&lt;span class="c"&gt;# Example method shape from the man page — only on systems where PCR extend is appropriate&lt;/span&gt;
&lt;span class="c"&gt;# sudo varlinkctl call /usr/lib/systemd/systemd-pcrextend \&lt;/span&gt;
&lt;span class="c"&gt;#   io.systemd.PCRExtend.Extend '{"pcr":15,"text":"foobar"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exec:&lt;/code&gt; form is equivalent when you want to be explicit:&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;varlinkctl info &lt;span class="nb"&gt;exec&lt;/span&gt;:/usr/lib/systemd/systemd-pcrextend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat PCR/measurement labs as &lt;strong&gt;maintenance-window&lt;/strong&gt; work on hosts where measured boot policy is intentional. Do not extend PCRs on production machines as a casual test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Remote calls over SSH
&lt;/h2&gt;

&lt;p&gt;When the remote side has OpenSSH 9.4+ (for &lt;code&gt;ssh-unix:&lt;/code&gt;) and the socket path exists:&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;# Talk to hostnamed on a remote machine via its AF_UNIX socket&lt;/span&gt;
&lt;span class="c"&gt;# varlinkctl call ssh-unix:somehost:/run/systemd/io.systemd.Hostname \&lt;/span&gt;
&lt;span class="c"&gt;#   io.systemd.Hostname.Describe '{}' -j&lt;/span&gt;

&lt;span class="c"&gt;# Or run a Varlink-capable binary on the remote stdio path&lt;/span&gt;
&lt;span class="c"&gt;# varlinkctl call ssh-exec:somehost:systemd-creds \&lt;/span&gt;
&lt;span class="c"&gt;#   org.varlink.service.GetInfo '{}' -j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for fleet introspection without installing a custom agent: SSH provides transport and auth; Varlink provides a typed method call. Abstract-namespace sockets are &lt;strong&gt;not&lt;/strong&gt; supported over &lt;code&gt;ssh-unix:&lt;/code&gt; (filesystem path sockets only).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Registry and socket discovery (260+)
&lt;/h2&gt;

&lt;p&gt;Newer systemd builds keep well-known entrypoints under &lt;code&gt;/run/varlink/registry/&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;# System registry (default)&lt;/span&gt;
varlinkctl list-registry

&lt;span class="c"&gt;# Per-user registry when applicable&lt;/span&gt;
varlinkctl list-registry &lt;span class="nt"&gt;--user&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/varlink/registry/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;list-sockets&lt;/code&gt; (262+) enumerates listening &lt;code&gt;AF_UNIX&lt;/code&gt; stream sockets marked as Varlink entrypoints via the &lt;code&gt;user.varlink=entrypoint&lt;/code&gt; xattr (needs kernel support for xattrs on socket inodes; documented as Linux 7.0+ in the man page). Prefer &lt;code&gt;list-registry&lt;/code&gt; on typical current distro kernels if &lt;code&gt;list-sockets&lt;/code&gt; is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — Serve a sandboxed stdio tool as Varlink (261+)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;varlinkctl serve&lt;/code&gt; turns a command that speaks a protocol on stdio into a socket-activated Varlink service. On upgrade, the client’s connection is handed to the command. The man page’s decompressor example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/etc/systemd/system/varlink-decompress-xz.socket&lt;/code&gt;&lt;/strong&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="nn"&gt;[Socket]&lt;/span&gt;
&lt;span class="py"&gt;ListenStream&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/varlink/registry/com.example.Decompress.XZ&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;sockets.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;/etc/systemd/system/varlink-decompress-xz.service&lt;/code&gt;&lt;/strong&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="nn"&gt;[Service]&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;varlinkctl serve com.example.Decompress.XZ xz -d&lt;/span&gt;
&lt;span class="py"&gt;DynamicUser&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateNetwork&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&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;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&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;span class="py"&gt;SystemCallFilter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;~@privileged @resources&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and call:&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; varlink-decompress-xz.socket

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; | xz | varlinkctl call &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  unix:/run/varlink/registry/com.example.Decompress.XZ &lt;span class="se"&gt;\&lt;/span&gt;
  com.example.Decompress.XZ &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;span class="c"&gt;# expected stdout: hello&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick test without unit files (ephemeral listen socket):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-socket-activate &lt;span class="nt"&gt;-l&lt;/span&gt; /tmp/decompress.sock &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  varlinkctl serve com.example.Decompress.XZ xz &lt;span class="nt"&gt;-d&lt;/span&gt; &amp;amp;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; | xz | varlinkctl call &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  unix:/tmp/decompress.sock &lt;span class="se"&gt;\&lt;/span&gt;
  com.example.Decompress.XZ &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this pattern matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The heavy lifting stays in a forked child with systemd sandboxing (&lt;code&gt;ProtectSystem=&lt;/code&gt;, &lt;code&gt;MemoryMax=&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;Clients discover a stable method name instead of ad-hoc FIFO paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--upgrade&lt;/code&gt; moves from JSON control plane to raw stream payload once the call succeeds.&lt;/li&gt;
&lt;li&gt;Pair with &lt;code&gt;--exec&lt;/code&gt; on the client when the reply includes file descriptors (&lt;code&gt;$LISTEN_FDS&lt;/code&gt; hand-off; 258+).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 8 — Validate IDL before you ship an interface
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/com.example.Echo.varlink &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;'
# Minimal echo interface for validation practice
interface com.example.Echo

method Ping(message: string) -&amp;gt; (message: string)

error EmptyMessage ()
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;varlinkctl validate-idl /tmp/com.example.Echo.varlink
&lt;span class="c"&gt;# prints the definition with syntax highlighting on success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catch naming and structure mistakes early; the wire protocol expects the same IDL shape services return from &lt;code&gt;org.varlink.service&lt;/code&gt; introspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operator cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Command pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who am I talking to?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;varlinkctl info ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What can it do?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-interfaces&lt;/code&gt; / &lt;code&gt;list-methods&lt;/code&gt; / &lt;code&gt;introspect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One shot call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call ADDRESS Fully.Qualified.Method '{"k":"v"}' -j&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream / subscribe&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;call -E …&lt;/code&gt; or &lt;code&gt;call --more --timeout=…&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle replies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call --collect …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No reply&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call --oneway …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote socket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssh-unix:host:/path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote binary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssh-exec:host:command&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrap stdio tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;varlinkctl serve METHOD cmdline…&lt;/code&gt; + socket unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check IDL file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;validate-idl FILE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-registry&lt;/code&gt; (260+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;No such file or directory&lt;/code&gt; on socket&lt;/td&gt;
&lt;td&gt;Service down or path wrong&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl status …&lt;/code&gt;; confirm path under &lt;code&gt;/run/systemd&lt;/code&gt; or &lt;code&gt;/run/varlink/registry&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection refused / permission denied&lt;/td&gt;
&lt;td&gt;Socket mode or user&lt;/td&gt;
&lt;td&gt;Check unit &lt;code&gt;SocketUser=&lt;/code&gt;/&lt;code&gt;SocketGroup=&lt;/code&gt;; use root only when required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method not found&lt;/td&gt;
&lt;td&gt;Typo or old package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-methods&lt;/code&gt; / &lt;code&gt;introspect&lt;/code&gt;; upgrade systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hang then timeout&lt;/td&gt;
&lt;td&gt;Multi-reply method without &lt;code&gt;--more&lt;/code&gt;, or stuck peer&lt;/td&gt;
&lt;td&gt;Match flags to IDL; set &lt;code&gt;--timeout=&lt;/code&gt; deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH unix path fails&lt;/td&gt;
&lt;td&gt;OpenSSH &amp;lt; 9.4 or abstract socket&lt;/td&gt;
&lt;td&gt;Upgrade SSH; use filesystem-bound sockets only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;serve&lt;/code&gt; never accepts&lt;/td&gt;
&lt;td&gt;Missing socket activation&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;.socket&lt;/code&gt; unit or &lt;code&gt;systemd-socket-activate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON parse errors&lt;/td&gt;
&lt;td&gt;Shell quoting&lt;/td&gt;
&lt;td&gt;Single-quote the JSON object; or pass args via STDIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pretty JSON breaks scripts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-j&lt;/code&gt; on a TTY&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;--json=short&lt;/code&gt; in pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;-u&lt;/span&gt; systemd-resolved.service &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'varlink-*.service'&lt;/span&gt; &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;
  
  
  How this fits nearby tooling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt; / D-Bus&lt;/strong&gt; — still the right tool for classic desktop and many system APIs. Varlink is an additional, JSON-native path used heavily by newer systemd components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resolvectl&lt;/code&gt; / &lt;code&gt;hostnamectl&lt;/code&gt;&lt;/strong&gt; — polished UX on top of the same daemons; &lt;code&gt;varlinkctl&lt;/code&gt; is the generic wrench when you need raw methods or automation without scraping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;systemd-ssh-proxy&lt;/code&gt; / generators&lt;/strong&gt; — solve VM/container SSH transport; orthogonal to Varlink method calls (though &lt;code&gt;ssh-unix:&lt;/code&gt; reuses SSH as a Varlink tunnel).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom REST agents&lt;/strong&gt; — often unnecessary on localhost when a sandboxed &lt;code&gt;varlinkctl serve&lt;/code&gt; unit already gives you discovery, socket activation, and cgroup limits.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/varlinkctl.1.en.html" rel="noopener noreferrer"&gt;varlinkctl(1)&lt;/a&gt; — commands, address forms, flags, examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/varlink/" rel="noopener noreferrer"&gt;UAPI.20 Varlink IPC&lt;/a&gt; — interface IDL, JSON mapping, wire protocol&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd-resolved/systemd-resolved.service.8.en.html" rel="noopener noreferrer"&gt;systemd-resolved.service(8)&lt;/a&gt; — DNS resolver service behind &lt;code&gt;io.systemd.Resolve&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/busctl.1.en.html" rel="noopener noreferrer"&gt;busctl(1)&lt;/a&gt; — D-Bus counterpart for comparison&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/systemd-socket-activate.1.en.html" rel="noopener noreferrer"&gt;systemd-socket-activate(1)&lt;/a&gt; — ad-hoc socket activation for &lt;code&gt;serve&lt;/code&gt; labs&lt;/li&gt;
&lt;li&gt;Upstream man source: &lt;a href="https://github.com/systemd/systemd" rel="noopener noreferrer"&gt;systemd &lt;code&gt;varlinkctl&lt;/code&gt; documentation&lt;/a&gt; (package man pages track your installed version)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have been parsing semi-stable command output or writing miniature D-Bus clients for one method call, point &lt;code&gt;varlinkctl&lt;/code&gt; at the socket, read the IDL, and call the method with JSON. Same skill scales from a quick resolved query to a socket-activated, sandboxed helper you can ship as a pair of unit files.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Losing User Services on Logout: Practical loginctl Linger on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:02:22 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-losing-user-services-on-logout-practical-loginctl-linger-on-linux-2m8o</link>
      <guid>https://dev.to/lyraalishaikh/stop-losing-user-services-on-logout-practical-loginctl-linger-on-linux-2m8o</guid>
      <description>&lt;h1&gt;
  
  
  Stop Losing User Services on Logout: Practical loginctl Linger on Linux
&lt;/h1&gt;

&lt;p&gt;You enable a rootless Podman container, a &lt;code&gt;systemd --user&lt;/code&gt; timer, or a small agent under your account. It works while the SSH session is open. You disconnect — and everything under &lt;code&gt;/run/user/$UID&lt;/code&gt; vanishes with it.&lt;/p&gt;

&lt;p&gt;That is not random flakiness. It is how &lt;code&gt;systemd-logind&lt;/code&gt; and &lt;code&gt;pam_systemd&lt;/code&gt; are designed to behave when &lt;strong&gt;user lingering&lt;/strong&gt; is off: the per-user service manager (&lt;code&gt;user@.service&lt;/code&gt;) is tied to login sessions, and the runtime directory is removed when the last session ends.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and defaults come from &lt;code&gt;loginctl(1)&lt;/code&gt;, &lt;code&gt;logind.conf(5)&lt;/code&gt;, &lt;code&gt;pam_systemd(8)&lt;/code&gt;, &lt;code&gt;user@.service(5)&lt;/code&gt;, and &lt;code&gt;systemd-logind.service(8)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What logind owns (and what it does not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-logind.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tracks users, sessions, seats; starts &lt;code&gt;user@.service&lt;/code&gt;; handles idle/power policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pam_systemd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registers each login with logind; creates session scope + &lt;code&gt;/run/user/$UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user@UID.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-user service manager (&lt;code&gt;systemd --user&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-runtime-dir@UID.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates/removes &lt;code&gt;/run/user/UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loginctl enable-linger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;user@.service&lt;/code&gt; at boot and after logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system units / root services&lt;/td&gt;
&lt;td&gt;Independent of linger; use those when the workload is truly system-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Sessions&lt;/strong&gt; are login contexts (SSH, TTY, graphical). &lt;strong&gt;The user manager&lt;/strong&gt; is a long-lived service instance shared across a user’s sessions. Linger decides whether that manager (and the runtime dir) outlive the last session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model in one tree
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;user@.service(5)&lt;/code&gt; and the &lt;code&gt;loginctl user-status&lt;/code&gt; example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user.slice
└─user-1000.slice
   ├─user@1000.service          # systemd --user (shared)
   │  ├─init.scope
   │  └─your.timer / your.service
   ├─session-3.scope            # one SSH login
   └─session-12.scope           # another TTY/GUI login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Processes started by &lt;code&gt;systemd --user&lt;/code&gt; live under &lt;code&gt;user@UID.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Interactive login processes live under &lt;code&gt;session-N.scope&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both sit under &lt;code&gt;user-UID.slice&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the last session ends and linger is &lt;strong&gt;disabled&lt;/strong&gt;, logind stops the user manager (after &lt;code&gt;UserStopDelaySec=&lt;/code&gt;) and tears down &lt;code&gt;/run/user/UID&lt;/code&gt;. Anything that needed &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; (user D-Bus, rootless containers, many socket paths) is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# loginctl ships with systemd on essentially every modern distro&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; loginctl
loginctl &lt;span class="nt"&gt;--version&lt;/span&gt;
systemctl is-active systemd-logind.service

&lt;span class="c"&gt;# You need a normal (non-system) user for the labs below&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;   &lt;span class="c"&gt;# e.g. 1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Privileged linger changes typically need root (or polkit authorization). Inspecting your own sessions usually does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inventory sessions, users, and seats
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl list-sessions
loginctl list-users
loginctl list-seats

&lt;span class="c"&gt;# Human-readable status for the caller&lt;/span&gt;
loginctl session-status
loginctl user-status

&lt;span class="c"&gt;# Machine-readable properties&lt;/span&gt;
loginctl show-user &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; Name &lt;span class="nt"&gt;-p&lt;/span&gt; UID &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; State &lt;span class="nt"&gt;-p&lt;/span&gt; Sessions &lt;span class="nt"&gt;-p&lt;/span&gt; RuntimePath
loginctl show-session self &lt;span class="nt"&gt;-p&lt;/span&gt; Id &lt;span class="nt"&gt;-p&lt;/span&gt; Name &lt;span class="nt"&gt;-p&lt;/span&gt; Class &lt;span class="nt"&gt;-p&lt;/span&gt; Type &lt;span class="nt"&gt;-p&lt;/span&gt; Active &lt;span class="nt"&gt;-p&lt;/span&gt; State &lt;span class="nt"&gt;-p&lt;/span&gt; Display &lt;span class="nt"&gt;-p&lt;/span&gt; Remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful properties (names from &lt;code&gt;loginctl show-*&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Linger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yes&lt;/code&gt; / &lt;code&gt;no&lt;/code&gt; — whether this user is set to linger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;State&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User/session state (&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;online&lt;/code&gt;, &lt;code&gt;closing&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session IDs belonging to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuntimePath&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/run/user/UID&lt;/code&gt; when the runtime dir exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Class&lt;/code&gt; / &lt;code&gt;Type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Session class (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;background&lt;/code&gt;, …) and type (&lt;code&gt;tty&lt;/code&gt;, &lt;code&gt;wayland&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JSON forms (newer systemd builds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl list-sessions &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty
loginctl list-users &lt;span class="nt"&gt;-j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Prove the “logout kills user services” failure mode
&lt;/h2&gt;

&lt;p&gt;Run this from an interactive login as the target user (not via a lingering manager you already enabled).&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;# Start a trivial user service that just sleeps&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/systemd/user/linger-demo.service &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;'
[Unit]
Description=Linger demo heartbeat

[Service]
Type=simple
ExecStart=/bin/bash -c 'while true; do echo "alive &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-Is&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"; sleep 30; done'
Restart=always

[Install]
WantedBy=default.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; linger-demo.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"XDG_RUNTIME_DIR=&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a &lt;strong&gt;second&lt;/strong&gt; root shell, watch the user manager:&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;UID_NUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# replace alice with the lab user&lt;/span&gt;
systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /run/user/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now fully log the user out of &lt;strong&gt;all&lt;/strong&gt; sessions (close SSH, log out of desktop, etc.). With linger disabled and after &lt;code&gt;UserStopDelaySec=&lt;/code&gt; (default &lt;strong&gt;10s&lt;/strong&gt; per &lt;code&gt;logind.conf(5)&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;systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# expected: inactive/dead once the user fully logged out&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /run/user/
&lt;span class="c"&gt;# expected: UID directory gone&lt;/span&gt;

&lt;span class="c"&gt;# The unit may still exist on disk under ~/.config/systemd/user,&lt;/span&gt;
&lt;span class="c"&gt;# but nothing is running it until the user manager returns.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the bug report you keep getting from “my user timer stopped overnight after I closed the laptop lid / SSH’d out.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Enable linger the supported way
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As root (or authorized admin)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger alice

&lt;span class="c"&gt;# Verify&lt;/span&gt;
loginctl show-user alice &lt;span class="nt"&gt;-p&lt;/span&gt; Linger
&lt;span class="c"&gt;# Linger=yes&lt;/span&gt;

&lt;span class="c"&gt;# Persistent marker files live here (one empty file per lingering user):&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/lib/systemd/linger/
&lt;span class="c"&gt;# ... alice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;enable-linger&lt;/code&gt; does, per &lt;code&gt;loginctl(1)&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If enabled for a specific user, a user manager is spawned for the user at boot and kept around after logouts. This allows users who are not logged in to run long-running services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Effects you should see:&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;# Even with zero interactive sessions:&lt;/span&gt;
loginctl list-users
systemctl is-active user@1000.service          &lt;span class="c"&gt;# active&lt;/span&gt;
systemctl is-active user-runtime-dir@1000.service
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /run/user/1000
&lt;span class="c"&gt;# XDG_RUNTIME_DIR still present for that UID&lt;/span&gt;

&lt;span class="c"&gt;# User services survive&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000 &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Talking to another user’s bus from root (documented &lt;code&gt;loginctl&lt;/code&gt; / &lt;code&gt;systemctl&lt;/code&gt; machine syntax pattern):&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="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alice@.host status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;DBUS_SESSION_BUS_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:path&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000/bus &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable when you no longer need 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;loginctl disable-linger alice
&lt;span class="nb"&gt;ls&lt;/span&gt; /var/lib/systemd/linger/   &lt;span class="c"&gt;# alice marker gone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After disable, the next full logout (and &lt;code&gt;UserStopDelaySec=&lt;/code&gt; expiry) returns to the non-linger teardown behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Boot-time user manager without an interactive login
&lt;/h2&gt;

&lt;p&gt;With linger enabled, reboot (or start on a host where the user never logged in this boot):&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;# After boot, as root&lt;/span&gt;
loginctl show-user alice &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; State
systemctl status user@1000.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /run/user/1000

&lt;span class="c"&gt;# Enable lingering user units so they start with the user manager&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000 &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; linger-demo.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Units linked to &lt;code&gt;default.target&lt;/code&gt; under &lt;code&gt;~/.config/systemd/user/&lt;/code&gt; start with the lingering user manager — no SSH required.&lt;/p&gt;

&lt;p&gt;Practical workloads that usually want this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rootless Podman / Quadlet user units&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd --user&lt;/code&gt; timers for backups, sync, cert renewals&lt;/li&gt;
&lt;li&gt;Personal agents bound to &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; sockets&lt;/li&gt;
&lt;li&gt;Lingering &lt;code&gt;pipewire&lt;/code&gt; / session buses on headless seats (when you intentionally run them that way)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workloads that usually should &lt;strong&gt;not&lt;/strong&gt; linger as a user service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything that must run before &lt;code&gt;systemd-user-sessions.service&lt;/code&gt; allows logins&lt;/li&gt;
&lt;li&gt;Multi-tenant daemons that belong under system units with explicit &lt;code&gt;User=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Secrets you only want available while a human is present&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 5 — KillUserProcesses, UserStopDelaySec, and tmux myths
&lt;/h2&gt;

&lt;p&gt;Linger is not the only dial. From &lt;code&gt;logind.conf(5)&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;# Vendor defaults are commented in the shipped file; local drop-ins win.&lt;/span&gt;
man logind.conf
&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/systemd/logind.conf.d/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default (upstream man page)&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KillUserProcesses=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;yes&lt;/code&gt;, session scope processes are killed on logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;KillOnlyUsers=&lt;/code&gt; / &lt;code&gt;KillExcludeUsers=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;(see man)&lt;/td&gt;
&lt;td&gt;Overrides who gets session-kill behavior; root excluded by default when unset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserStopDelaySec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long to keep &lt;code&gt;user@.service&lt;/code&gt; after the last session ends (&lt;code&gt;0&lt;/code&gt; = immediate; &lt;code&gt;infinity&lt;/code&gt; = never stop after first login)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RemoveIPC=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove SysV/POSIX IPC objects on full logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuntimeDirectorySize=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;10%&lt;/code&gt; of RAM&lt;/td&gt;
&lt;td&gt;Safety limit for each &lt;code&gt;/run/user/UID&lt;/code&gt; tmpfs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Important nuance from the man page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KillUserProcesses=yes&lt;/code&gt; kills processes in the &lt;strong&gt;session scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Independently, linger controls whether &lt;strong&gt;&lt;code&gt;user@.service&lt;/code&gt;&lt;/strong&gt; stays up.&lt;/li&gt;
&lt;li&gt;Tools like &lt;code&gt;tmux&lt;/code&gt;/&lt;code&gt;screen&lt;/code&gt; that stay inside the session scope die when session-kill is on, unless moved out (for example with &lt;code&gt;systemd-run --user --scope&lt;/code&gt; as discussed in &lt;code&gt;systemd-run(1)&lt;/code&gt; / &lt;code&gt;logind.conf(5)&lt;/code&gt; notes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop-in example (only if you understand the impact on shared hosts):&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/systemd/logind.conf.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/logind.conf.d/20-lab-kill.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;'
[Login]
# Example only — many desktops/distros already ship an opinion here.
KillUserProcesses=yes
UserStopDelaySec=10s
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart systemd-logind.service
&lt;span class="c"&gt;# Caution: restarting logind can disrupt active sessions; do this in a maintenance window.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;strong&gt;linger for user services&lt;/strong&gt; over globally turning off process cleanup when the real goal is “keep my timer alive.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Session hygiene: lock, terminate, kill
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inspect&lt;/span&gt;
loginctl list-sessions &lt;span class="nt"&gt;--no-legend&lt;/span&gt;
loginctl session-status 3   &lt;span class="c"&gt;# replace with a real ID&lt;/span&gt;

&lt;span class="c"&gt;# Request screen lock where the session supports it&lt;/span&gt;
loginctl lock-session 3
loginctl unlock-session 3
loginctl lock-sessions      &lt;span class="c"&gt;# all lock-capable sessions&lt;/span&gt;

&lt;span class="c"&gt;# Clean teardown of one session (kills its processes, frees resources)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl terminate-session 3

&lt;span class="c"&gt;# Signal without full teardown&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl kill-session 3 &lt;span class="nt"&gt;--kill-whom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="nt"&gt;--signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SIGTERM

&lt;span class="c"&gt;# Tear down every session for a user (does not by itself clear linger)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl terminate-user alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;terminate-user&lt;/code&gt; ends sessions and deallocates runtime resources attached to those sessions. With linger still enabled, expect &lt;code&gt;user@.service&lt;/code&gt; to come back (or stay) according to linger policy rather than “user permanently gone.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — Cap resources for lingering users
&lt;/h2&gt;

&lt;p&gt;Lingering users still consume a user manager and a runtime directory. Bound them with the slice hierarchy from &lt;code&gt;user@.service(5)&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;# All users under user.slice&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/user-.slice.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/user-.slice.d/20-limits.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;'
[Slice]
# Defaults already include TasksMax=33% via vendor drop-in on many systems.
MemoryHigh=2G
MemoryMax=3G
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# One specific UID&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/user-1000.slice.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/user-1000.slice.d/20-limits.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;'
[Slice]
CPUQuota=200%
MemoryMax=4G
TasksMax=4096
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="c"&gt;# Apply to live slices when present:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart user-1000.slice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session-scoped limits can also be applied via PAM data keys documented in &lt;code&gt;pam_systemd(8)&lt;/code&gt; (&lt;code&gt;systemd.memory_max&lt;/code&gt;, &lt;code&gt;systemd.tasks_max&lt;/code&gt;, …). Those apply to &lt;strong&gt;session scopes&lt;/strong&gt;, not to the shared &lt;code&gt;user@.service&lt;/code&gt; tree — another reason lingering services need slice-level policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal “rootless service that survives logout” recipe
&lt;/h2&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;USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&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="nv"&gt;$USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Wait until runtime dir exists (boot or first activation)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 50&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  sleep &lt;/span&gt;0.2
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload

&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; linger-demo.service

loginctl show-user &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; RuntimePath &lt;span class="nt"&gt;-p&lt;/span&gt; State
systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;linger-demo.service&lt;/code&gt; with your Quadlet, timer, or agent unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User timer dies after SSH logout&lt;/td&gt;
&lt;td&gt;Linger disabled; &lt;code&gt;user@.service&lt;/code&gt; stopped&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loginctl enable-linger USER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Failed to connect to bus&lt;/code&gt; as user&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;/run/user/UID&lt;/code&gt; / user manager&lt;/td&gt;
&lt;td&gt;Enable linger or stay logged in; export &lt;code&gt;XDG_RUNTIME_DIR&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linger set but services still stop&lt;/td&gt;
&lt;td&gt;Units not enabled under &lt;code&gt;--user&lt;/code&gt;; wrong &lt;code&gt;WantedBy=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl --user enable --now …&lt;/code&gt;; link to &lt;code&gt;default.target&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless Podman breaks after logout&lt;/td&gt;
&lt;td&gt;Runtime dir removed; containers needed linger&lt;/td&gt;
&lt;td&gt;Enable linger for that UID; confirm &lt;code&gt;/run/user/UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tmux&lt;/code&gt; dies on logout&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;KillUserProcesses=yes&lt;/code&gt; and tmux still in session scope&lt;/td&gt;
&lt;td&gt;Move work to &lt;code&gt;systemd --user&lt;/code&gt; / &lt;code&gt;systemd-run --user --scope&lt;/code&gt;, or adjust kill policy carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/run/user/UID&lt;/code&gt; grows large&lt;/td&gt;
&lt;td&gt;Runtime tmpfs pressure&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;RuntimeDirectorySize=&lt;/code&gt;; stop leaking caches into the runtime dir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;enable-linger&lt;/code&gt; fails&lt;/td&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;Run as root; check polkit / &lt;code&gt;systemd-logind&lt;/code&gt; logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User manager flap on rapid logout/login&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserStopDelaySec=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raise delay (default 10s) if appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;-u&lt;/span&gt; systemd-logind.service &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'user@*.service'&lt;/span&gt; &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;
  
  
  How this fits nearby systemd pieces
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System services&lt;/strong&gt; (&lt;code&gt;systemctl enable&lt;/code&gt;) — always-on, root or static &lt;code&gt;User=&lt;/code&gt;, independent of linger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run0&lt;/code&gt; / &lt;code&gt;systemd-run&lt;/code&gt;&lt;/strong&gt; — transient elevation or one-shot scopes; not a substitute for a lingering user manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Socket activation&lt;/strong&gt; — on-demand start is orthogonal; user socket units still need a living &lt;code&gt;systemd --user&lt;/code&gt; if they are user units.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;homed / encrypted homes&lt;/strong&gt; — home unlock is separate from linger; a lingering manager with an unavailable home can still surprise you at boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lingering ≠ immortality&lt;/strong&gt; — &lt;code&gt;terminate-user&lt;/code&gt;, resource OOM policy, and admin &lt;code&gt;disable-linger&lt;/code&gt; still apply.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/loginctl.1.en" rel="noopener noreferrer"&gt;loginctl(1)&lt;/a&gt; — &lt;code&gt;enable-linger&lt;/code&gt; / &lt;code&gt;disable-linger&lt;/code&gt;, session and user commands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/logind.conf.5.en" rel="noopener noreferrer"&gt;logind.conf(5)&lt;/a&gt; — &lt;code&gt;KillUserProcesses=&lt;/code&gt;, &lt;code&gt;UserStopDelaySec=&lt;/code&gt;, runtime dir limits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-logind.service.8.en" rel="noopener noreferrer"&gt;systemd-logind.service(8)&lt;/a&gt; — login manager responsibilities&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/pam_systemd.8.en" rel="noopener noreferrer"&gt;pam_systemd(8)&lt;/a&gt; — session registration, &lt;code&gt;/run/user/$UID&lt;/code&gt;, logout teardown&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/user@.service.5.en" rel="noopener noreferrer"&gt;user@.service(5)&lt;/a&gt; — user manager + &lt;code&gt;user-runtime-dir@.service&lt;/code&gt; + slice hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-run.1.en" rel="noopener noreferrer"&gt;systemd-run(1)&lt;/a&gt; — transient user scopes/services&lt;/li&gt;
&lt;li&gt;Debian man page mirror: &lt;a href="https://manpages.debian.org/bookworm/systemd/loginctl.1.en.html" rel="noopener noreferrer"&gt;loginctl(1) on manpages.debian.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your “set it and forget it” user service only lives as long as an SSH tty, turn linger on deliberately, enable the user unit, and verify &lt;code&gt;Linger=yes&lt;/code&gt; plus a living &lt;code&gt;/run/user/$UID&lt;/code&gt; after logout. That single control is the difference between a rootless stack that survives disconnects and one that only works while you are watching it.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop wget-and-Hope Image Downloads: Practical importctl on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 27 Sep 2026 05:01:53 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-wget-and-hope-image-downloads-practical-importctl-on-linux-5392</link>
      <guid>https://dev.to/lyraalishaikh/stop-wget-and-hope-image-downloads-practical-importctl-on-linux-5392</guid>
      <description>&lt;h1&gt;
  
  
  Stop wget-and-Hope Image Downloads: Practical importctl on Linux
&lt;/h1&gt;

&lt;p&gt;You need a container rootfs, a portable service image, a sysext, or a confext on disk. The old habit is familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://example.com/jammy-root.tar.xz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; /var/lib/machines/jammy &lt;span class="nt"&gt;-xf&lt;/span&gt; jammy-root.tar.xz
&lt;span class="c"&gt;# ... did the checksum match? where did the qcow2 land? who owns this transfer?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until you want &lt;strong&gt;GPG-verified pulls&lt;/strong&gt;, &lt;strong&gt;qcow2→raw conversion&lt;/strong&gt;, &lt;strong&gt;class-aware install paths&lt;/strong&gt;, &lt;strong&gt;cancellable background transfers&lt;/strong&gt;, or the same workflow for portable/sysext/confext images instead of only nspawn machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;importctl&lt;/code&gt;&lt;/strong&gt; (systemd &lt;strong&gt;256+&lt;/strong&gt;) is the CLI in front of &lt;strong&gt;&lt;code&gt;systemd-importd.service&lt;/code&gt;&lt;/strong&gt;. It downloads, imports, and exports disk images into the right image class directory — machines, portables, sysexts, confexts — with optional checksum/signature verification and transfer listing/cancel.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and options below come from &lt;code&gt;importctl(1)&lt;/code&gt;, &lt;code&gt;systemd-importd.service(8)&lt;/code&gt;, and &lt;code&gt;systemd-firstboot(1)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What importctl owns (and what it does not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wget&lt;/code&gt; / &lt;code&gt;curl&lt;/code&gt; + &lt;code&gt;tar&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Blind download and unpack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;machinectl pull-tar&lt;/code&gt; / &lt;code&gt;pull-raw&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Legacy machine-only pull path (still around on many hosts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;importctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Class-aware pull/import/export via &lt;code&gt;systemd-importd&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd-nspawn&lt;/code&gt; / &lt;code&gt;machinectl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Boot and manage machine images once they are on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;portablectl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Attach/detach portable service images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd-sysext&lt;/code&gt; / &lt;code&gt;systemd-confext&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Merge extension images into the host tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-dissect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect/mount/validate DDIs already on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;importctl places images. Other tools activate them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Image classes and install directories (&lt;code&gt;importctl(1)&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Short flag&lt;/th&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;machine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/machines/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;portable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/portables/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sysext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/extensions/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;confext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/confexts/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Supported payload shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tar&lt;/strong&gt; filesystem images (&lt;code&gt;.tar&lt;/code&gt;, &lt;code&gt;.tar.gz&lt;/code&gt;, &lt;code&gt;.tar.xz&lt;/code&gt;, &lt;code&gt;.tar.bz2&lt;/code&gt;, plus zstd on import/export paths)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw / qcow2&lt;/strong&gt; block images (optional &lt;code&gt;.gz&lt;/code&gt; / &lt;code&gt;.xz&lt;/code&gt; / &lt;code&gt;.bz2&lt;/code&gt;; qcow2 is converted to raw on pull)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCI&lt;/strong&gt; container references via &lt;code&gt;pull-oci&lt;/code&gt; (&lt;strong&gt;systemd 260+&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu — importd/nspawn tooling usually ships with systemd-container&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-container

&lt;span class="c"&gt;# Fedora&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;systemd-container

&lt;span class="c"&gt;# Arch — typically part of the main systemd package&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; systemd

&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; importctl
importctl &lt;span class="nt"&gt;--version&lt;/span&gt;
systemctl status systemd-importd.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;importctl&lt;/code&gt; talks to &lt;strong&gt;&lt;code&gt;systemd-importd&lt;/code&gt;&lt;/strong&gt; over D-Bus (&lt;code&gt;org.freedesktop.import1&lt;/code&gt;). Privileged image writes need appropriate authorization (typically root or a polkit-allowed admin).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — List what you already have
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Images already present for the selected class (default class is machine-oriented workflows; be explicit)&lt;/span&gt;
importctl list-images &lt;span class="nt"&gt;-m&lt;/span&gt;
importctl list-images &lt;span class="nt"&gt;-P&lt;/span&gt;
importctl list-images &lt;span class="nt"&gt;-S&lt;/span&gt;
importctl list-images &lt;span class="nt"&gt;-C&lt;/span&gt;

&lt;span class="c"&gt;# JSON for scripts&lt;/span&gt;
importctl list-images &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty output is fine on a fresh host. After pulls/imports, this is your inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Pull a verified tar rootfs into machines/
&lt;/h2&gt;

&lt;p&gt;The man page example uses Ubuntu cloud root tarballs. Verification defaults to &lt;strong&gt;&lt;code&gt;--verify=signature&lt;/code&gt;&lt;/strong&gt;: integrity via &lt;code&gt;.sha256&lt;/code&gt; / &lt;code&gt;SHA256SUMS&lt;/code&gt;, plus GPG against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/usr/lib/systemd/import-pubring.pgp&lt;/code&gt; (vendor keyring)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/systemd/import-pubring.pgp&lt;/code&gt; (local trust; falls back to legacy &lt;code&gt;.gpg&lt;/code&gt; path if needed)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# -m  → class=machine → /var/lib/machines/&lt;/span&gt;
&lt;span class="c"&gt;# -N  → --keep-download=no (write straight to the local name; default keep-download is true for machines)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-mN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz

&lt;span class="c"&gt;# Optional explicit local name&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-mN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz &lt;span class="se"&gt;\&lt;/span&gt;
  jammy-root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification modes (&lt;code&gt;--verify=&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;signature&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Checksum &lt;strong&gt;and&lt;/strong&gt; detached GPG signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checksum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256 only (&lt;code&gt;.sha256&lt;/code&gt; or &lt;code&gt;SHA256SUMS&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No download verification (use only for trusted local mirrors you already checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the remote only publishes checksums without signatures you trust in the import keyring, you will need &lt;code&gt;--verify=checksum&lt;/code&gt; (or import a local file after verifying out-of-band).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ctrl-C does not abort an in-flight transfer.&lt;/strong&gt; Use transfer management instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;importctl list-transfer
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl cancel-transfer &amp;lt;ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 3 — Pull a raw disk image, set root password offline, boot it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-raw &lt;span class="nt"&gt;-mN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-disk-kvm.img &lt;span class="se"&gt;\&lt;/span&gt;
  jammy

&lt;span class="c"&gt;# Offline first-boot style setup against the image file&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/machines/jammy.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt-root-password&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Register/start via machined (same store nspawn uses)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;machinectl start jammy
&lt;span class="nb"&gt;sudo &lt;/span&gt;machinectl login jammy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes from the man pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloaded &lt;strong&gt;qcow2&lt;/strong&gt; images are &lt;strong&gt;converted to raw&lt;/strong&gt; before they are made available.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-firstboot --image=&lt;/code&gt; operates on the disk image without booting it — ideal after &lt;code&gt;pull-raw&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;--root-password-file=&lt;/code&gt; / &lt;code&gt;--root-password-hashed=&lt;/code&gt; over putting a plaintext password on the command line when you automate this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shell into a tar-style machine image without a full boot:&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-nspawn &lt;span class="nt"&gt;-M&lt;/span&gt; jammy-root
&lt;span class="c"&gt;# or, after import name derivation from the URL basename:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-nspawn &lt;span class="nt"&gt;-M&lt;/span&gt; jammy-server-cloudimg-amd64-root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 4 — Import local tar/raw/directory trees (no network verification)
&lt;/h2&gt;

&lt;p&gt;Local imports &lt;strong&gt;do not&lt;/strong&gt; run the pull verification path. Verify checksums yourself before &lt;code&gt;import-*&lt;/code&gt; if the file came from anywhere untrusted.&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;# Tar archive → unpacked directory/subvolume under the class dir&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-tar &lt;span class="nt"&gt;-m&lt;/span&gt; ./myroot.tar.xz myroot

&lt;span class="c"&gt;# Raw or qcow2 file (compressed ok) → machine image&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-raw &lt;span class="nt"&gt;-m&lt;/span&gt; ./disk.qcow2 mydisk

&lt;span class="c"&gt;# Existing directory tree (btrfs snapshot/subvolume when supported)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-fs &lt;span class="nt"&gt;-m&lt;/span&gt; /srv/build/rootfs myfs

&lt;span class="c"&gt;# Read from stdin (NAME is mandatory when FILE is '-')&lt;/span&gt;
xz &lt;span class="nt"&gt;-dc&lt;/span&gt; ./myroot.tar.xz | &lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-tar &lt;span class="nt"&gt;-m&lt;/span&gt; - myroot-stdin

&lt;span class="c"&gt;# Force replace an existing local name&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-raw &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; ./disk.raw mydisk

&lt;span class="c"&gt;# Read-only image result&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl import-tar &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; ./golden.tar.xz golden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 5 — Class-aware pulls for portable / sysext / confext
&lt;/h2&gt;

&lt;p&gt;Same tool, different install 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="c"&gt;# Portable service image → /var/lib/portables/&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;checksum &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/images/myapp.tar.xz myapp

&lt;span class="c"&gt;# Then attach with portablectl (separate tool/workflow)&lt;/span&gt;
&lt;span class="c"&gt;# sudo portablectl attach myapp --enable --now&lt;/span&gt;

&lt;span class="c"&gt;# System extension → /var/lib/extensions/&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-raw &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;checksum &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/images/devtools.raw devtools
&lt;span class="c"&gt;# sudo systemd-sysext merge&lt;/span&gt;

&lt;span class="c"&gt;# Configuration extension → /var/lib/confexts/&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;checksum &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/images/site-conf.tar.xz site-conf
&lt;span class="c"&gt;# sudo systemd-confext merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--keep-download=&lt;/code&gt; defaults to &lt;strong&gt;true for &lt;code&gt;machine&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;false otherwise&lt;/strong&gt;. For machines, a successful pull can keep a read-only URL+ETag download and snapshot/copy a writable instance. Pass &lt;strong&gt;&lt;code&gt;-N&lt;/code&gt; / &lt;code&gt;--keep-download=no&lt;/code&gt;&lt;/strong&gt; when you want a single named image and no retained download template. To download &lt;strong&gt;only&lt;/strong&gt; the read-only template and skip the writable instance, use &lt;code&gt;-&lt;/code&gt; as the local name (see &lt;code&gt;pull-tar&lt;/code&gt; / &lt;code&gt;pull-raw&lt;/code&gt; in &lt;code&gt;importctl(1)&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Export images for backup or handoff
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Directory/subvolume machine → compressed tar (suffix selects compressor)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl export-tar &lt;span class="nt"&gt;-m&lt;/span&gt; fedora ./fedora-backup.tar.xz

&lt;span class="c"&gt;# Raw disk machine → raw export (optional compression via suffix or --format=)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl export-raw &lt;span class="nt"&gt;-m&lt;/span&gt; jammy ./jammy.raw.xz

&lt;span class="c"&gt;# Explicit format when writing to stdout or a suffix-less path&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl export-tar &lt;span class="nt"&gt;-m&lt;/span&gt; fedora &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zst - &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fedora.tar.zst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compression formats: &lt;code&gt;uncompressed&lt;/code&gt;, &lt;code&gt;xz&lt;/code&gt;, &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;zst&lt;/code&gt;, &lt;code&gt;bzip2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint from the man page:&lt;/strong&gt; only directory/subvolume images export as tar; only raw disk images export as raw. Match the export command to how the image lives on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — OCI pull (systemd 260+)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;importctl &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# need 260+ for pull-oci&lt;/span&gt;

&lt;span class="c"&gt;# REF is an OCI reference such as library/nginx&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-oci &lt;span class="nt"&gt;-m&lt;/span&gt; library/nginx nginx

&lt;span class="c"&gt;# HTTPS cert checks apply; importctl does not apply the tar/raw GPG verify path to OCI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use OCI pulls when your upstream already publishes container images and you want them materialized into a systemd image class directory. For classic cloud tarballs and DDIs, prefer &lt;code&gt;pull-tar&lt;/code&gt; / &lt;code&gt;pull-raw&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer hygiene and remote operation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch / cancel&lt;/span&gt;
importctl list-transfer &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl cancel-transfer 1234

&lt;span class="c"&gt;# Quiet mode for scripts&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-mN&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;checksum https://mirror.example/root.tar.xz app-root

&lt;span class="c"&gt;# Operate against importd in a container, or via SSH host&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl &lt;span class="nt"&gt;-M&lt;/span&gt; mycontainer list-images &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl &lt;span class="nt"&gt;-H&lt;/span&gt; admin@bastion.example list-images &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A minimal “pull → boot” recipe you can script
&lt;/h2&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;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jammy-lab
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz

&lt;span class="c"&gt;# Replace if re-running&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;importctl list-images &lt;span class="nt"&gt;-m&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qw&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&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;sudo &lt;/span&gt;machinectl remove &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;importctl pull-tar &lt;span class="nt"&gt;-mN&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-nspawn &lt;span class="nt"&gt;-M&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For raw KVM-style disks, swap in &lt;code&gt;pull-raw&lt;/code&gt;, run &lt;code&gt;systemd-firstboot --image=...&lt;/code&gt;, then &lt;code&gt;machinectl start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signature verification fails&lt;/td&gt;
&lt;td&gt;Key not in import pubring&lt;/td&gt;
&lt;td&gt;Install vendor key, or use a mirror you verify with &lt;code&gt;--verify=checksum&lt;/code&gt; after manual GPG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name already exists&lt;/td&gt;
&lt;td&gt;Prior image left behind&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--force&lt;/code&gt;, or remove via &lt;code&gt;machinectl&lt;/code&gt; / delete under the class directory carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl-C “did nothing”&lt;/td&gt;
&lt;td&gt;Transfer continues in importd&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;importctl list-transfer&lt;/code&gt; + &lt;code&gt;cancel-transfer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export-tar rejects a &lt;code&gt;.raw&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Wrong shape for tar export&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;export-raw&lt;/code&gt; for raw disk images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission errors&lt;/td&gt;
&lt;td&gt;importd needs privileges&lt;/td&gt;
&lt;td&gt;Run authorized as root; check polkit / &lt;code&gt;systemd-importd&lt;/code&gt; logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCI command missing&lt;/td&gt;
&lt;td&gt;Older systemd&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pull-oci&lt;/code&gt; needs &lt;strong&gt;260+&lt;/strong&gt;; use tar/raw pulls or upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;-u&lt;/span&gt; systemd-importd.service &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;
  
  
  How this fits the rest of the systemd image stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; images with &lt;code&gt;mkosi&lt;/code&gt; / &lt;code&gt;systemd-repart&lt;/code&gt; (or consume vendor cloud images).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move&lt;/strong&gt; them with &lt;strong&gt;&lt;code&gt;importctl&lt;/code&gt;&lt;/strong&gt; (this article).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect/mount&lt;/strong&gt; DDIs with &lt;code&gt;systemd-dissect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt; machines with &lt;code&gt;systemd-nspawn&lt;/code&gt; / &lt;code&gt;machinectl&lt;/code&gt; or VMs with &lt;code&gt;systemd-vmspawn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach&lt;/strong&gt; portables with &lt;code&gt;portablectl&lt;/code&gt;; &lt;strong&gt;merge&lt;/strong&gt; sysext/confext with &lt;code&gt;systemd-sysext&lt;/code&gt; / &lt;code&gt;systemd-confext&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt; fleets with &lt;code&gt;systemd-sysupdate&lt;/code&gt; when you are doing A/B image transfers rather than one-off imports.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;importctl&lt;/code&gt; is the missing “get the bits into the right directory, verified and cancellable” layer — not a replacement for boot or extension activation.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;importctl(1)&lt;/code&gt; — pull/import/export commands, classes, verify, keep-download (&lt;a href="https://man.archlinux.org/man/importctl.1.en" rel="noopener noreferrer"&gt;Arch man page&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-importd.service(8)&lt;/code&gt; — backend service and D-Bus API pointer (&lt;a href="https://man.archlinux.org/man/systemd-importd.service.8.en" rel="noopener noreferrer"&gt;Arch man page&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-firstboot(1)&lt;/code&gt; — offline &lt;code&gt;--image=&lt;/code&gt; initialization after &lt;code&gt;pull-raw&lt;/code&gt; (&lt;a href="https://man.archlinux.org/man/systemd-firstboot.1.en" rel="noopener noreferrer"&gt;Arch man page&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-nspawn(1)&lt;/code&gt; / &lt;code&gt;machinectl(1)&lt;/code&gt; — boot and manage machine-class images&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;portablectl(1)&lt;/code&gt;, &lt;code&gt;systemd-sysext(8)&lt;/code&gt;, &lt;code&gt;systemd-confext(8)&lt;/code&gt; — activate non-machine classes&lt;/li&gt;
&lt;li&gt;Ubuntu cloud images (example URLs used above): &lt;a href="https://cloud-images.ubuntu.com/" rel="noopener noreferrer"&gt;https://cloud-images.ubuntu.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflow still starts with bare &lt;code&gt;wget&lt;/code&gt; into &lt;code&gt;/var/lib/machines&lt;/code&gt;, try one &lt;code&gt;importctl pull-tar -mN&lt;/code&gt; with signature verification enabled. Same end state — better transfer control, class routing, and fewer “which tarball was this?” moments.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop losetup Guesswork: Practical systemd-dissect for Discoverable Disk Images on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:02:28 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-losetup-guesswork-practical-systemd-dissect-for-discoverable-disk-images-on-linux-2d7l</link>
      <guid>https://dev.to/lyraalishaikh/stop-losetup-guesswork-practical-systemd-dissect-for-discoverable-disk-images-on-linux-2d7l</guid>
      <description>&lt;h1&gt;
  
  
  Stop losetup Guesswork: Practical systemd-dissect for Discoverable Disk Images on Linux
&lt;/h1&gt;

&lt;p&gt;You have a disk image. Maybe it came from &lt;code&gt;mkosi&lt;/code&gt;, a cloud vendor, a portable service build, a sysext raw, or last night's A/B update pipeline. The old muscle memory is familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;losetup &lt;span class="nt"&gt;-fP&lt;/span&gt; ./image.raw
fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; /dev/loopX
mount /dev/loopXp2 /mnt
&lt;span class="c"&gt;# ... later, which partitions did I attach again?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until the image has LUKS, dm-verity, a multi-partition GPT layout, or architecture-specific root types. Then the one-liner becomes a script, and the script becomes the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;systemd-dissect&lt;/code&gt;&lt;/strong&gt; is the systemd-native tool for introspecting and operating on &lt;strong&gt;Discoverable Disk Images (DDIs)&lt;/strong&gt;. It understands GPT labels from the &lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/" rel="noopener noreferrer"&gt;UAPI Discoverable Partitions Specification&lt;/a&gt;, can mount whole OS trees correctly, copy files in and out, validate image policy, and even plug into classic &lt;code&gt;mount&lt;/code&gt; / &lt;code&gt;fstab&lt;/code&gt; as &lt;code&gt;mount.ddi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and options below are taken from &lt;code&gt;systemd-dissect(1)&lt;/code&gt;, &lt;code&gt;systemd.image-policy(7)&lt;/code&gt;, and the DPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a DDI is (and what dissect does)
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Discoverable Disk Image&lt;/strong&gt; is an OS disk image systemd can reason about automatically. &lt;code&gt;systemd-dissect&lt;/code&gt; accepts three shapes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GPT image with DPS partition types&lt;/strong&gt; (root, usr, home, srv, esp, verity, …)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain filesystem image&lt;/strong&gt; (no partition table — treated as the root FS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT/MBR with a single partition&lt;/strong&gt; (that partition is the root FS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inside those images you can have normal Linux filesystems, &lt;strong&gt;LUKS&lt;/strong&gt;, and &lt;strong&gt;dm-verity&lt;/strong&gt; data. The same class of images boots with &lt;code&gt;systemd-nspawn --image=&lt;/code&gt; and can back &lt;code&gt;RootImage=&lt;/code&gt; on services.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;systemd-dissect&lt;/code&gt; with no command switch, it does &lt;strong&gt;not&lt;/strong&gt; dump every GPT entry like &lt;code&gt;fdisk&lt;/code&gt;. It shows the partitions it &lt;em&gt;understands and would operate on&lt;/em&gt;: unknown types are skipped, duplicates are ignored, and root/&lt;code&gt;usr&lt;/code&gt; partitions for foreign architectures are filtered. That is intentional — dissect reports the OS view, not the full partition editor view.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fdisk&lt;/code&gt; / &lt;code&gt;sfdisk&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Edit/list the raw partition table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;losetup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Attach a file as a block device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;veritysetup&lt;/code&gt; / &lt;code&gt;cryptsetup&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Manual integrity / encryption setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-dissect&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS-aware inspect, mount, copy, validate, archive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-container

&lt;span class="c"&gt;# Arch&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; systemd

systemd-dissect &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# mount.ddi should resolve to the same binary (symlink helper)&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; mount.ddi &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /usr/bin/mount.ddi /sbin/mount.ddi 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most inspect/validate paths need read access to the image. Mount, attach, copy-in, and write paths need privileges (typically root) so loop devices and nested mounts can be set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 0 — Build a tiny plain-FS image to practice on
&lt;/h2&gt;

&lt;p&gt;You do not need a full multi-partition DDI to learn the tool. A single ext4 image is enough for mount/list/copy/with flows:&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; ~/dissect-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/dissect-lab

&lt;span class="c"&gt;# 256 MiB sparse file + ext4 (plain FS image = valid DDI shape #2)&lt;/span&gt;
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 256M plain.raw
mkfs.ext4 &lt;span class="nt"&gt;-L&lt;/span&gt; labroot plain.raw

&lt;span class="c"&gt;# Put something recognizable inside via a temporary loop mount&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/plain-lab
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop plain.raw /mnt/plain-lab
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/plain-lab/etc
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'ID=lab\nVERSION_ID=1\nPRETTY_NAME="dissect-lab"\n'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /mnt/plain-lab/etc/os-release &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'hello from plain DDI'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /mnt/plain-lab/ROOT.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/plain-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a &lt;strong&gt;real GPT + DPS&lt;/strong&gt; image, build with &lt;code&gt;mkosi&lt;/code&gt; or &lt;code&gt;systemd-repart&lt;/code&gt; (covered in earlier posts). Dissect is happiest when partition type GUIDs match DPS (for example &lt;code&gt;SD_GPT_ROOT_X86_64&lt;/code&gt; = &lt;code&gt;4f68bce3-e8cd-4db1-96e7-fbcaf984b709&lt;/code&gt; on amd64).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inspect before you mount
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Human-readable OS / partition summary&lt;/span&gt;
systemd-dissect plain.raw

&lt;span class="c"&gt;# Machine-readable&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty plain.raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect os-release fields (when present), architecture hints, and the design designations dissect derived. On multi-partition DDIs this is where you confirm "yes, this has root + usr + verity" before you hand the file to nspawn, vmspawn, or a service &lt;code&gt;RootImage=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Useful companion:&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;# Full GPT including types dissect ignores — use when debugging image builds&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; plain.raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Mount and unmount the OS view
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/ddi-plain

&lt;span class="c"&gt;# Create target if missing (-M == --mount --mkdir)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;-M&lt;/span&gt; plain.raw /mnt/ddi-plain

&lt;span class="c"&gt;# Read-only mount&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; plain.raw /mnt/ddi-plain

&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/ddi-plain
&lt;span class="nb"&gt;cat&lt;/span&gt; /mnt/ddi-plain/ROOT.txt
&lt;span class="nb"&gt;cat&lt;/span&gt; /mnt/ddi-plain/etc/os-release

&lt;span class="c"&gt;# Recursive unmount + remove empty dir (-U == --umount --rmdir)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;-U&lt;/span&gt; /mnt/ddi-plain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What mount does for you (from the man page):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dissects the image and mounts the root (and nested DPS mounts such as &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, … when present)&lt;/li&gt;
&lt;li&gt;Sets up &lt;strong&gt;LUKS&lt;/strong&gt; and &lt;strong&gt;Verity&lt;/strong&gt; automatically when the image carries them, and tears them down on unmount&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;fsck in automatic-fix mode&lt;/strong&gt; on writable access unless you pass &lt;code&gt;--fsck=no&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Honors GPT &lt;strong&gt;growfs&lt;/strong&gt; bit 59 by growing the filesystem to the partition size unless &lt;code&gt;--growfs=no&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disable grow/fsck when you want a surgical inspection:&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-dissect &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--fsck&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;--growfs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ./image.raw /mnt/ddi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  mount.ddi and fstab
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;systemd-dissect&lt;/code&gt; can be invoked as &lt;strong&gt;&lt;code&gt;mount.ddi&lt;/code&gt;&lt;/strong&gt;, implementing mount(8)'s external helper for type &lt;code&gt;ddi&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; /mnt/ddi-plain
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; ddi plain.raw /mnt/ddi-plain

&lt;span class="c"&gt;# Multi-FS DDIs need recursive unmount&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount &lt;span class="nt"&gt;-R&lt;/span&gt; /mnt/ddi-plain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boot-time / persistent mount via fstab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/path/to/myimage.raw  /images/myimage/  ddi  defaults  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mapped mount options from the man page:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;fstab / mount option&lt;/th&gt;
&lt;th&gt;dissect equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--read-only&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;writable (default for &lt;code&gt;--mount&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--discard=all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nodiscard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--discard=disabled&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those options apply to how the &lt;strong&gt;image&lt;/strong&gt; is attached; they are not generically passed through to every inner filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Run a command inside a temporary mount (&lt;code&gt;--with&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;This is the cleanest one-shot pattern — mount, cwd into the tree, run a command, always unmount:&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;# Interactive shell in the image&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; plain.raw

&lt;span class="c"&gt;# One command (man page tarball example)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; plain.raw &lt;span class="nb"&gt;tar &lt;/span&gt;cz &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plain-from-with.tar.gz

&lt;span class="c"&gt;# Environment the child sees (documented):&lt;/span&gt;
&lt;span class="c"&gt;#   $SYSTEMD_DISSECT_ROOT   — absolute temp mount path&lt;/span&gt;
&lt;span class="c"&gt;#   $SYSTEMD_DISSECT_DEVICE — loop device path&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; plain.raw &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo root=$SYSTEMD_DISSECT_ROOT; echo dev=$SYSTEMD_DISSECT_DEVICE; ls -la'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit status of &lt;code&gt;--with&lt;/code&gt; is the exit status of the child command.&lt;/p&gt;

&lt;p&gt;Writable by default; add &lt;code&gt;--read-only&lt;/code&gt; for inspection. Prefer &lt;code&gt;--in-memory&lt;/code&gt; when you need a writable scratch view of a read-only golden image without dirtying 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-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nt"&gt;--in-memory&lt;/span&gt; plain.raw &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo scratch &amp;gt; ./SCRATCH.txt; cat ./SCRATCH.txt'&lt;/span&gt;
&lt;span class="c"&gt;# original plain.raw unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 4 — List files and mtree manifests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# All paths in the image&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--list&lt;/span&gt; plain.raw | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;span class="c"&gt;# BSD mtree-compatible manifest with SHA256 content digests&lt;/span&gt;
&lt;span class="c"&gt;# (timestamps/nlink/ino intentionally omitted for reproducibility)&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--mtree&lt;/span&gt; plain.raw | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;span class="c"&gt;# Faster mtree without content hashes on large images&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--mtree&lt;/span&gt; &lt;span class="nt"&gt;--mtree-hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no ./big.raw | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--mtree&lt;/code&gt; is excellent for CI diffing of image builds. The man page notes current limitations: no xattrs, capabilities, MAC labels, chattr flags, or btrfs subvolume metadata in the manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Copy files in and out without a long-lived mount
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Host ← image&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--copy-from&lt;/span&gt; plain.raw /ROOT.txt ./ROOT-from-image.txt
&lt;span class="c"&gt;# stdout:&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--copy-from&lt;/span&gt; plain.raw /etc/os-release -

&lt;span class="c"&gt;# Host → image (writable; fsck runs first by default)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'injected'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ./inject.txt
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--copy-to&lt;/span&gt; plain.raw ./inject.txt /opt/inject.txt

&lt;span class="c"&gt;# Verify&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--copy-from&lt;/span&gt; plain.raw /opt/inject.txt -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules of thumb from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regular files copy mode/xattrs/timestamps; &lt;strong&gt;ownership is not&lt;/strong&gt; copied for single files&lt;/li&gt;
&lt;li&gt;Directories copy recursively &lt;strong&gt;and&lt;/strong&gt; include ownership&lt;/li&gt;
&lt;li&gt;Source &lt;code&gt;-&lt;/code&gt; reads stdin (&lt;code&gt;--copy-to&lt;/code&gt;); destination &lt;code&gt;-&lt;/code&gt; writes stdout (&lt;code&gt;--copy-from&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 6 — Attach as a loop device (when you still need block tools)
&lt;/h2&gt;

&lt;p&gt;Sometimes you want &lt;code&gt;cfdisk&lt;/code&gt;, &lt;code&gt;btrfs inspect-internal&lt;/code&gt;, or another block-level tool. Prefer dissect's attach over bare &lt;code&gt;losetup&lt;/code&gt; so sector size and partition nodes are correct:&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;# Prints the loop path, creates partition sub-nodes before returning&lt;/span&gt;
&lt;span class="nv"&gt;LOOP&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;systemd-dissect &lt;span class="nt"&gt;--attach&lt;/span&gt; &lt;span class="nt"&gt;--loop-ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;labplain plain.raw&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;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /dev/disk/by-loop-ref/labplain

&lt;span class="c"&gt;# Use the stable by-loop-ref name&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cfdisk /dev/disk/by-loop-ref/labplain

&lt;span class="c"&gt;# Detach by loop path or by backing image path&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# or: sudo systemd-dissect --detach plain.raw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--loop-ref=&lt;/code&gt; sets the kernel &lt;code&gt;.lo_file_name&lt;/code&gt; field (up to 63 chars) used for &lt;code&gt;/dev/disk/by-loop-ref/...&lt;/code&gt;. That is distinct from sysfs &lt;code&gt;backing_file&lt;/code&gt;, which is mount-namespace translated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — Validate image policy before you trust an image
&lt;/h2&gt;

&lt;p&gt;Image policy is a colon-separated string of &lt;code&gt;partition=flags&lt;/code&gt; rules. Flags include &lt;code&gt;unprotected&lt;/code&gt;, &lt;code&gt;verity&lt;/code&gt;, &lt;code&gt;signed&lt;/code&gt;, &lt;code&gt;encrypted&lt;/code&gt;, &lt;code&gt;unused&lt;/code&gt;, &lt;code&gt;absent&lt;/code&gt;, plus GPT flag requirements like &lt;code&gt;read-only-on&lt;/code&gt; / &lt;code&gt;growfs-off&lt;/code&gt;. Shortcuts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;use everything recognized (common default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;use nothing (&lt;code&gt;unused+absent&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;everything must be absent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Syntax/meaning helper&lt;/span&gt;
systemd-analyze image-policy &lt;span class="s1"&gt;'usr=verity+read-only-on:root=encrypted:swap=encrypted'&lt;/span&gt;

&lt;span class="c"&gt;# Validate arrangement + policy without mounting (unprivileged if the file is readable)&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--validate&lt;/span&gt; plain.raw
systemd-dissect &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--image-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root=unprotected'&lt;/span&gt; plain.raw

&lt;span class="c"&gt;# Refuse unprotected roots in an automation gate&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--image-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root=verity+signed'&lt;/span&gt; ./golden.raw &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;p&gt;&lt;code&gt;--validate&lt;/code&gt; parses the partition table and probes filesystems but does &lt;strong&gt;not&lt;/strong&gt; mount or set up LUKS/Verity. It prints &lt;code&gt;OK&lt;/code&gt; and exits 0 when the image matches policy.&lt;/p&gt;

&lt;p&gt;Example policies from &lt;code&gt;systemd.image-policy(7)&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;# Read-only Verity /usr, encrypted root+swap; ignore the rest
usr=verity+read-only-on:root=encrypted:swap=encrypted

# Encrypted writable root; /srv encrypted if present; no swap
root=encrypted+read-only-off:srv=encrypted+absent:swap=absent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 8 — Discover images on the system and archive them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Machines, portables, sysext/confext locations systemd knows about&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--discover&lt;/span&gt;

&lt;span class="c"&gt;# Archive the filesystem view (format from output suffix; libarchive-backed)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--make-archive&lt;/span&gt; plain.raw plain.tar.gz
&lt;span class="c"&gt;# stdout tarball (uncompressed when path omitted):&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--make-archive&lt;/span&gt; plain.raw - &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plain.tar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--discover&lt;/code&gt; walks the usual trees: &lt;code&gt;/usr/lib/machines/&lt;/code&gt;, &lt;code&gt;/var/lib/machines/&lt;/code&gt;, &lt;code&gt;/usr/lib/portables/&lt;/code&gt;, &lt;code&gt;/var/lib/portables/&lt;/code&gt;, &lt;code&gt;/usr/lib/extensions/&lt;/code&gt;, &lt;code&gt;/var/lib/extensions/&lt;/code&gt;, confext paths, and friends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verity and encrypted images
&lt;/h2&gt;

&lt;p&gt;When the DDI embeds Verity per DPS, mount/attach paths set it up for you. For detached metadata:&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-dissect &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root-hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HEX_ROOT_HASH &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root-hash-sig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./roothash.p7s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--verity-data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./image.verity &lt;span class="se"&gt;\&lt;/span&gt;
  ./image.raw /mnt/ddi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer embedding Verity in the GPT image (root + root-verity + optional signature partitions) so tools do not need extra flags. That is the same model used by &lt;code&gt;systemd-repart&lt;/code&gt; image builds and image-based update flows.&lt;/p&gt;

&lt;p&gt;Discard policy for thin/encrypted backends:&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;# disabled | loop | all | crypto&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--discard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;loop ./image.raw /mnt/ddi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wire-up: nspawn, services, and friends
&lt;/h2&gt;

&lt;p&gt;Once dissect is happy with an image, the rest of the systemd image ecosystem lights up:&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;# Namespace OS container from the same DDI&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-nspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./image.raw

&lt;span class="c"&gt;# Service with image root (unit fragment)&lt;/span&gt;
&lt;span class="c"&gt;# RootImage=/var/lib/machines/app.raw&lt;/span&gt;
&lt;span class="c"&gt;# RootImagePolicy=root=verity+signed+encrypted+unprotected+absent:...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RootImage=&lt;/code&gt;, &lt;code&gt;MountImage=&lt;/code&gt;, and &lt;code&gt;ExtensionImage=&lt;/code&gt; all take related image-policy settings (&lt;code&gt;RootImagePolicy=&lt;/code&gt; and friends in &lt;code&gt;systemd.exec(5)&lt;/code&gt;). Dissect is how you &lt;strong&gt;debug&lt;/strong&gt; those images before a unit fails at boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical CI-shaped workflow
&lt;/h2&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;IMG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?image&lt;span class="p"&gt; path&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# 1. Policy gate (no mount, minimal privilege)&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--image-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root=unprotected+verity+signed+encrypted'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Show what systemd will actually use&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/ddi-meta.json

&lt;span class="c"&gt;# 3. Content fingerprint for regression&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--mtree&lt;/span&gt; &lt;span class="nt"&gt;--mtree-hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt;

&lt;span class="c"&gt;# 4. Smoke: read os-release without leaving mounts behind&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;etc/os-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Boundaries — pick the right tool
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Prefer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inspect / mount / copy / validate a DDI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-dissect&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot the image as a container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-nspawn --image=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot the image as a full VM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-vmspawn --image=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attach a portable service image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;portablectl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge &lt;code&gt;/usr&lt;/code&gt; add-ons at runtime&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-sysext&lt;/code&gt; / &lt;code&gt;confext&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build/grow GPT layouts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-repart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-level Verity format/open&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veritysetup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw partition editing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sfdisk&lt;/code&gt; / &lt;code&gt;sgdisk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dissect does not replace your image builder. It replaces the fragile glue between "image file on disk" and "filesystem tree I can trust and use."&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty or sparse inspect output&lt;/strong&gt; — foreign-arch root type, unknown GPT types, or not a filesystem systemd can probe. Confirm with &lt;code&gt;fdisk -l&lt;/code&gt; and rebuild with DPS types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission denied on mount/attach&lt;/strong&gt; — need root (or equivalent) for loop + mount namespace operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fsck delays on every write open&lt;/strong&gt; — expected; use &lt;code&gt;--fsck=no&lt;/code&gt; for throwaway labs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image grew unexpectedly&lt;/strong&gt; — GPT growfs flag (bit 59); disable with &lt;code&gt;--growfs=no&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;leftover loop devices&lt;/strong&gt; — always &lt;code&gt;--umount&lt;/code&gt; / &lt;code&gt;--detach&lt;/code&gt;; for &lt;code&gt;mount -t ddi&lt;/code&gt;, use &lt;code&gt;umount -R&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy validate fails on a "fine" image&lt;/strong&gt; — your policy may require &lt;code&gt;verity&lt;/code&gt;/&lt;code&gt;signed&lt;/code&gt; while the lab image is &lt;code&gt;unprotected&lt;/code&gt;. Start from &lt;code&gt;*&lt;/code&gt; and tighten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--with&lt;/code&gt; left nothing mounted but command failed&lt;/strong&gt; — check the child exit status; dissect propagates it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you already live in the systemd image world — nspawn, vmspawn, portable services, sysext, repart, sysupdate — &lt;strong&gt;&lt;code&gt;systemd-dissect&lt;/code&gt; is the missing inspection plane&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Day-to-day defaults:&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;# What is this file?&lt;/span&gt;
systemd-dissect ./image.raw

&lt;span class="c"&gt;# Safe look around&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-dissect &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; ./image.raw

&lt;span class="c"&gt;# Policy gate&lt;/span&gt;
systemd-dissect &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--image-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root=unprotected+verity+signed'&lt;/span&gt; ./image.raw

&lt;span class="c"&gt;# Persistent host mount&lt;/span&gt;
&lt;span class="c"&gt;# /var/lib/images/app.raw  /srv/app-image  ddi  ro  0  0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop writing &lt;code&gt;losetup&lt;/code&gt; scaffolding for every OS image. Let dissect own the loop, the nested mounts, and the teardown.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/trixie/systemd-container/systemd-dissect.1.en.html" rel="noopener noreferrer"&gt;systemd-dissect(1)&lt;/a&gt; — Debian man page (commands, options, mount.ddi, examples)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/trixie/systemd/systemd.image-policy.7.en.html" rel="noopener noreferrer"&gt;systemd.image-policy(7)&lt;/a&gt; — dissection policy language&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/trixie/systemd/systemd.exec.5.en.html" rel="noopener noreferrer"&gt;systemd.exec(5)&lt;/a&gt; — &lt;code&gt;RootImage=&lt;/code&gt;, image policy unit settings&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/" rel="noopener noreferrer"&gt;UAPI.2 Discoverable Partitions Specification&lt;/a&gt; — GPT type UUIDs and auto-mount rules&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/trixie/systemd-container/systemd-nspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-nspawn(1)&lt;/a&gt; — boot DDIs as containers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/systemd-vmspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-vmspawn(1)&lt;/a&gt; — boot DDIs as VMs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/trixie/systemd/systemd-analyze.1.en.html" rel="noopener noreferrer"&gt;systemd-analyze(1)&lt;/a&gt; — &lt;code&gt;image-policy&lt;/code&gt; helper&lt;/li&gt;
&lt;li&gt;Related posts on this blog: systemd-vmspawn, systemd-nspawn, sysext/confext, portablectl, systemd-repart, dm-verity&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Hand-Writing QEMU Flags: Practical systemd-vmspawn VMs on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 25 Sep 2026 05:02:07 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-writing-qemu-flags-practical-systemd-vmspawn-vms-on-linux-2di0</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-writing-qemu-flags-practical-systemd-vmspawn-vms-on-linux-2di0</guid>
      <description>&lt;h1&gt;
  
  
  Stop Hand-Writing QEMU Flags: Practical systemd-vmspawn VMs on Linux
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;systemd-nspawn&lt;/code&gt; is great when a namespace container is enough. Sometimes it is not.&lt;/p&gt;

&lt;p&gt;You need a &lt;strong&gt;real kernel&lt;/strong&gt;, a &lt;strong&gt;TPM&lt;/strong&gt;, &lt;strong&gt;Secure Boot firmware&lt;/strong&gt;, or a guest that must look like bare metal. That is when people fall back to a pile of &lt;code&gt;qemu-system-x86_64&lt;/code&gt; flags, a one-off libvirt XML, or a desktop hypervisor UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;systemd-vmspawn&lt;/code&gt;&lt;/strong&gt; is systemd's answer: the same "spawn an OS image" ergonomics as &lt;code&gt;systemd-nspawn&lt;/code&gt;, but it launches a full virtual machine (typically QEMU/KVM) instead of namespaces.&lt;/p&gt;

&lt;p&gt;This guide is operational. Every flag and flow below comes from the current &lt;code&gt;systemd-vmspawn(1)&lt;/code&gt; man page and related systemd docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What vmspawn is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;th&gt;Kernel&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-nspawn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;namespaces / cgroups&lt;/td&gt;
&lt;td&gt;host kernel&lt;/td&gt;
&lt;td&gt;OS trees, chroot-like labs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-vmspawn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;full VM (KVM)&lt;/td&gt;
&lt;td&gt;guest kernel&lt;/td&gt;
&lt;td&gt;real OS images, firmware, TPM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libvirt / virt-manager&lt;/td&gt;
&lt;td&gt;full VM&lt;/td&gt;
&lt;td&gt;guest kernel&lt;/td&gt;
&lt;td&gt;long-lived multi-VM farms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Podman / Docker&lt;/td&gt;
&lt;td&gt;OCI app containers&lt;/td&gt;
&lt;td&gt;host kernel&lt;/td&gt;
&lt;td&gt;single apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vmspawn is intentionally &lt;strong&gt;nspawn-shaped&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start from &lt;code&gt;--image=&lt;/code&gt; or &lt;code&gt;--directory=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;--machine=&lt;/code&gt; name&lt;/li&gt;
&lt;li&gt;optional registration with &lt;code&gt;systemd-machined&lt;/code&gt; / &lt;code&gt;machinectl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;credentials, bind mounts, journal forwarding&lt;/li&gt;
&lt;li&gt;scope unit properties for CPU/memory caps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a replacement for a full multi-tenant hypervisor control plane. Think: "I have a disk image and I want a correct KVM boot &lt;strong&gt;now&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;Added in systemd &lt;strong&gt;255&lt;/strong&gt;, with substantial options through &lt;strong&gt;256–262&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;On a typical Debian/Ubuntu 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="c"&gt;# packages (names vary slightly by distro)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-container qemu-system-x86 ovmf

&lt;span class="c"&gt;# KVM access for your user (needed for vsock on Debian/Ubuntu)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; kvm &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# re-login after this&lt;/span&gt;

&lt;span class="c"&gt;# optional: software TPM for guests&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;swtpm swtpm-tools

&lt;span class="c"&gt;# optional: build disk images the systemd way&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mkosi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanity checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /dev/kvm
&lt;span class="c"&gt;# firmware discovery (lists JSON firmware definitions)&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--firmware&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On systems where &lt;code&gt;/dev/kvm&lt;/code&gt; is missing (nested virt disabled, cloud instance without KVM), vmspawn can still run with &lt;code&gt;--kvm=no&lt;/code&gt;, but expect pure TCG emulation and slower boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Build a tiny image and boot it
&lt;/h2&gt;

&lt;p&gt;The man page's first example is still the cleanest path. With &lt;code&gt;mkosi&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/vmspawn-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/vmspawn-lab

&lt;span class="c"&gt;# Arch example from systemd-vmspawn(1); swap -d for your distro if preferred&lt;/span&gt;
mkosi &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;arch&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; systemd &lt;span class="nt"&gt;-p&lt;/span&gt; linux &lt;span class="nt"&gt;--autologin&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; image.raw &lt;span class="nt"&gt;-f&lt;/span&gt; build

systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;image.raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That drops you on an interactive guest console (&lt;code&gt;--console=interactive&lt;/code&gt; is the default).&lt;/p&gt;

&lt;h3&gt;
  
  
  Import a cloud image instead
&lt;/h3&gt;

&lt;p&gt;If you already use &lt;code&gt;machinectl&lt;/code&gt; / &lt;code&gt;importctl&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;# Example pattern from the man page family: pull a cloud image, then boot the .raw&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;machinectl pull-raw &lt;span class="se"&gt;\&lt;/span&gt;
  https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/ &lt;span class="se"&gt;\&lt;/span&gt;
  Fedora-Cloud  &lt;span class="c"&gt;# adjust to a concrete image URL on your host&lt;/span&gt;

&lt;span class="c"&gt;# After import, boot the raw under /var/lib/machines (path varies)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-vmspawn &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;/var/lib/machines/Fedora-Cloud.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fedora-cloud &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer an explicit full URL to a known cloud image on your distro's download site. The point is the &lt;strong&gt;boot command&lt;/strong&gt;, not chasing a moving Fedora filename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core knobs you will actually use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Image vs directory
&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;# Disk image (raw by default; qcow2 via --image-format=)&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.qcow2 &lt;span class="nt"&gt;--image-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;qcow2

&lt;span class="c"&gt;# Directory tree as root (virtiofs under the hood)&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules from the man page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One of &lt;code&gt;--directory=&lt;/code&gt; or &lt;code&gt;--image=&lt;/code&gt; is required; if neither is given, &lt;code&gt;--directory=.&lt;/code&gt; is assumed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--ephemeral&lt;/code&gt; (&lt;code&gt;-x&lt;/code&gt;) takes a &lt;strong&gt;temporary snapshot&lt;/strong&gt; of the image and deletes it when the VM exits. Works with &lt;code&gt;--image=&lt;/code&gt; only. Does &lt;strong&gt;not&lt;/strong&gt; combine with &lt;code&gt;--extra-drive=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--grow-image=20G&lt;/code&gt; / &lt;code&gt;-G 20G&lt;/code&gt; expands a too-small image file (rounded up to 4K). With &lt;code&gt;--ephemeral&lt;/code&gt;, growth applies to the snapshot copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CPU, RAM, KVM, vsock, TPM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &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;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lab1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kvm&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;--vsock&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;--tpm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defaults worth remembering:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--cpus=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--ram=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;newer systemd: &lt;code&gt;BYTES[:MAXBYTES[:SLOTS]]&lt;/code&gt; for memory hotplug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--kvm=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;detect &lt;code&gt;/dev/kvm&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--vsock=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;guest AF_VSOCK; Debian/Ubuntu need &lt;code&gt;kvm&lt;/code&gt; group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--tpm=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;needs &lt;code&gt;swtpm&lt;/code&gt; on PATH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--tpm-state=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;path derived as &lt;code&gt;&amp;lt;image&amp;gt;.tpmstate&lt;/code&gt;; &lt;code&gt;off&lt;/code&gt; is transient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--discard-disk=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;pass TRIM/discard through to the image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--notify-ready=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;true&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;waits for guest init &lt;code&gt;READY=1&lt;/code&gt; (nspawn defaults the opposite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;--tpm-state=off&lt;/code&gt; (or ephemeral + auto) is wrong for guests that bind disk encryption keys to the vTPM — those keys vanish on every shutdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Networking: user-mode vs TAP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;User-mode&lt;/strong&gt; (no root, NAT via QEMU):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TAP&lt;/strong&gt; (root, proper L2, needs systemd-networkd):&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-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="nt"&gt;--network-tap&lt;/span&gt; &lt;span class="nt"&gt;-M&lt;/span&gt; taplab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements called out by the man page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;root privileges for TAP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-networkd&lt;/code&gt; running on the host&lt;/li&gt;
&lt;li&gt;stock unit: &lt;code&gt;/usr/lib/systemd/network/80-vm-vt.network&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If TAP comes up but the guest has no DHCP, check that networkd owns the host &lt;code&gt;vt-*&lt;/code&gt; side and that your nftables forward/NAT policy allows it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firmware and Secure Boot
&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;# See what firmware JSON files systemd can find&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--firmware&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list

&lt;span class="c"&gt;# Prefer Secure Boot-capable firmware&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="nt"&gt;--secure-boot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;

&lt;span class="c"&gt;# Direct kernel boot (no firmware), or UKI path&lt;/span&gt;
systemd-vmspawn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./rootfs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./rootfs/boot/vmlinuz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./rootfs/boot/initrd.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On recent systemd builds, &lt;code&gt;--firmware=&lt;/code&gt; also accepts &lt;code&gt;auto|uefi|bios|none&lt;/code&gt;, a path to a firmware definition, or &lt;code&gt;describe&lt;/code&gt; to print the selected UEFI image. Booting a &lt;strong&gt;UKI&lt;/strong&gt; requires UEFI firmware. Excess CLI arguments after options are passed as &lt;strong&gt;extra kernel cmdline via SMBIOS&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Console modes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--console&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;interactive   &lt;span class="c"&gt;# default: host TTY ↔ guest console&lt;/span&gt;
&lt;span class="nt"&gt;--console&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;read-only     &lt;span class="c"&gt;# watch only&lt;/span&gt;
&lt;span class="nt"&gt;--console&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;native        &lt;span class="c"&gt;# QEMU monitor available&lt;/span&gt;
&lt;span class="nt"&gt;--console&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gui           &lt;span class="c"&gt;# graphical QEMU UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--background=44&lt;/code&gt; tints the terminal while the VM runs (interactive/read-only only).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Directory root + private users + journal forward
&lt;/h2&gt;

&lt;p&gt;This is the man page's "systemd system image" pattern, adapted for a local 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="c"&gt;# Assume an OS tree at ./system (mkosi.output/system style)&lt;/span&gt;
&lt;span class="c"&gt;# Map host subuid range into the guest via virtiofsd&lt;/span&gt;
&lt;span class="nv"&gt;SHIFT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:"&lt;/span&gt; /etc/subuid | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

systemd-vmspawn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHIFT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./system.efi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--forward-journal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./vm.journal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sysimg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;enforcing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each piece does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--private-users=UID_SHIFT[:RANGE]&lt;/code&gt; — turn on UID/GID mapping for directory boots (default range 65536). Required when the directory is not root-owned in a way the host can safely expose.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--linux=&lt;/code&gt; — direct-boot a kernel or UKI; for directory images without &lt;code&gt;--linux=&lt;/code&gt;, vmspawn searches BLS entries under &lt;code&gt;/boot&lt;/code&gt; (XBOOTLDR) and &lt;code&gt;/efi&lt;/code&gt; (ESP).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--forward-journal=&lt;/code&gt; — host-side file or directory; guest journal is received via &lt;code&gt;systemd-journal-remote&lt;/code&gt; semantics.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enforcing=0&lt;/code&gt; — extra kernel cmdline token (SMBIOS), useful so SELinux does not block first boot of a lab image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inspect the forwarded journal later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./vm.journal &lt;span class="nt"&gt;-b&lt;/span&gt;
&lt;span class="c"&gt;# or, if you forwarded to a directory:&lt;/span&gt;
&lt;span class="c"&gt;# journalctl --directory=./vm-journals&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 3 — Resource caps via scope properties
&lt;/h2&gt;

&lt;p&gt;When vmspawn is &lt;strong&gt;not&lt;/strong&gt; run with &lt;code&gt;--keep-unit&lt;/code&gt;, it registers a scope under &lt;code&gt;machine.slice&lt;/code&gt; by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &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;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;capped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--slice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;machine.slice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;CPUQuota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200% &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--property=&lt;/code&gt; accepts the same assignments as &lt;code&gt;systemctl set-property&lt;/code&gt;. Use it for &lt;code&gt;MemoryMax=&lt;/code&gt;, &lt;code&gt;CPUQuota=&lt;/code&gt;, &lt;code&gt;TasksMax=&lt;/code&gt;, and friends so a runaway guest cannot eat the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Credentials into the guest
&lt;/h2&gt;

&lt;p&gt;vmspawn mirrors unit credentials:&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;'s3cret-db-password\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ./db.pass
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ./db.pass

systemd-vmspawn &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;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;credlab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dbpass:./db.pass &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app.env:APP_ENV&lt;span class="o"&gt;=&lt;/span&gt;lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a systemd guest, those show up through the normal credentials directory (&lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; / &lt;code&gt;ImportCredential=&lt;/code&gt; / &lt;code&gt;LoadCredential=&lt;/code&gt; in units). Binary values in &lt;code&gt;--set-credential=&lt;/code&gt; use C-style escapes (&lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\x00&lt;/code&gt;); shells may unescape once, so double-escaping is sometimes required.&lt;/p&gt;

&lt;p&gt;For host-side encryption of credential files before load, pair with &lt;code&gt;systemd-creds encrypt&lt;/code&gt; and unit &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; — that is the service-manager path; vmspawn itself takes plaintext load/set forms as documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — SSH over vsock with systemd-ssh-proxy
&lt;/h2&gt;

&lt;p&gt;The man page ends with this workflow:&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;CID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3735928559   &lt;span class="c"&gt;# pick an unused CID in 3..0xFFFF_FFFE&lt;/span&gt;

systemd-vmspawn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:"&lt;/span&gt; /etc/subuid | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./system.efi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vsock-cid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sshlab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;enforcing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal (while the VM runs):&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;# Ephemeral key path pattern from systemd-vmspawn(1)&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /run/user/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/systemd/vmspawn/

ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; /run/user/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/systemd/vmspawn/machine-&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-sshlab-ed25519&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"root@vsock/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CID&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;Notes from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By default vmspawn &lt;strong&gt;generates an ephemeral SSH key&lt;/strong&gt; so it can talk D-Bus into the guest (&lt;code&gt;--pass-ssh-key=yes&lt;/code&gt; default). Keys live only for that invocation under &lt;code&gt;/run/user/$UID/systemd/vmspawn/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--ssh-key-type=ed25519&lt;/code&gt; is default; &lt;code&gt;rsa&lt;/code&gt; exists for ancient guest &lt;code&gt;sshd&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On Debian/Ubuntu, vsock needs membership in group &lt;code&gt;kvm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh root@vsock/$CID&lt;/code&gt; needs a client that understands the systemd vsock proxy path (&lt;code&gt;systemd-ssh-proxy&lt;/code&gt; integration).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disable key generation only if you provide another way in: &lt;code&gt;--pass-ssh-key=no&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind mounts, extra disks, bind-user
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Host path → same path in guest&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/cache/build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind-ro&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/src/linux-headers

&lt;span class="c"&gt;# Host:guest path pair (escape colons with \:)&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/you/proj:/opt/proj

&lt;span class="c"&gt;# Additional data disk&lt;/span&gt;
systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-drive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raw:./data.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-drive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;qcow2:./bulk.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--bind-user=alice&lt;/code&gt; (systemd &lt;strong&gt;259+&lt;/strong&gt;) is stronger than a plain bind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Host home is exposed under &lt;code&gt;/run/vmhost/home/&lt;/code&gt; via virtiofs with UID translation.&lt;/li&gt;
&lt;li&gt;Transient user/group records are injected as &lt;code&gt;userdb.transient.*&lt;/code&gt; credentials so &lt;code&gt;nss-systemd&lt;/code&gt; in the guest can resolve the account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Caveats from the man page (read these before using it on untrusted guests):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guest needs systemd &lt;strong&gt;258+&lt;/strong&gt; with &lt;code&gt;nss-systemd&lt;/code&gt; in &lt;code&gt;nsswitch.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The propagated record includes the &lt;strong&gt;UNIX password hash&lt;/strong&gt; — use a strong hash (&lt;code&gt;yescrypt&lt;/code&gt; / &lt;code&gt;$y$&lt;/code&gt;) on the host.&lt;/li&gt;
&lt;li&gt;Mapping is transient; leftover files owned by a recycled guest UID can become someone else's later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  machined registration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As root, registration defaults on; as user, defaults off (Debian man page)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-vmspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./disk.raw &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reg1 &lt;span class="nt"&gt;--register&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes

&lt;/span&gt;machinectl list
machinectl status reg1
machinectl shell reg1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recent systemd also adds &lt;code&gt;--system&lt;/code&gt; / &lt;code&gt;--user&lt;/code&gt; to pick which manager / machined instance to talk to (v260+).&lt;/p&gt;

&lt;h2&gt;
  
  
  Ephemeral smoke-test pattern
&lt;/h2&gt;

&lt;p&gt;Golden image stays clean; every run is disposable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &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;./golden.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ephemeral&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--grow-image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smoke-&lt;span class="nv"&gt;$$&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;run.id:smoke-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y%m%dT%H%M%SZ&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the process exits, the snapshot is gone. Do not attach long-lived &lt;code&gt;--extra-drive=&lt;/code&gt; in this mode (unsupported with &lt;code&gt;--ephemeral&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  systemd unit wrapper (optional)
&lt;/h2&gt;

&lt;p&gt;For a lab VM you want under systemd supervision:&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;# /etc/systemd/system/vmspawn-lab@.service&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vmspawn lab VM %i
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target
&lt;span class="nv"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="c"&gt;# Type=notify works well because vmspawn notifies readiness&lt;/span&gt;
&lt;span class="c"&gt;# after the guest is ready (--notify-ready=true by default)&lt;/span&gt;
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;notify
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/systemd-vmspawn &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;/var/lib/machines/%i.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%i &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-tap&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--register&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;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3G
&lt;span class="nv"&gt;KillMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mixed
&lt;span class="nv"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&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;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start vmspawn-lab@webtest
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status vmspawn-lab@webtest
machinectl status webtest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exit status
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;systemd-vmspawn(1)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On tool/setup failure, the &lt;code&gt;errno&lt;/code&gt; value is propagated.&lt;/li&gt;
&lt;li&gt;If the guest supplies &lt;code&gt;EXIT_STATUS&lt;/code&gt;, that is returned.&lt;/li&gt;
&lt;li&gt;Otherwise success.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Boundaries — pick the right tool
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Prefer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same-kernel OS tree, fast iteration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-nspawn&lt;/code&gt; / &lt;code&gt;machinectl&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host &lt;code&gt;/usr&lt;/code&gt; add-on without a VM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-sysext&lt;/code&gt; / portable services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real guest kernel, firmware, TPM, Secure Boot&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-vmspawn&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-node virt farm, live migration, fancy storage pools&lt;/td&gt;
&lt;td&gt;libvirt / oVirt / Proxmox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single app packaging&lt;/td&gt;
&lt;td&gt;Podman / Docker / Quadlet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Confidential computing (&lt;code&gt;--coco=sev-snp|tdx&lt;/code&gt;) exists on recent systemd builds but is marked &lt;strong&gt;experimental&lt;/strong&gt; in the man page — treat it as a research path, not a homelab default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No KVM&lt;/strong&gt; — &lt;code&gt;ls -l /dev/kvm&lt;/code&gt;; nest virt; or &lt;code&gt;--kvm=no&lt;/code&gt; for a slow lab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vsock / SSH fails on Debian&lt;/strong&gt; — user in &lt;code&gt;kvm&lt;/code&gt; group; re-login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TAP has no address&lt;/strong&gt; — networkd active; &lt;code&gt;80-vm-vt.network&lt;/code&gt; present; check nftables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory boot permission errors&lt;/strong&gt; — set &lt;code&gt;--private-users=&lt;/code&gt; from &lt;code&gt;/etc/subuid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest never "ready"&lt;/strong&gt; — guest init must &lt;code&gt;sd_notify&lt;/code&gt; READY=1, or pass &lt;code&gt;--notify-ready=no&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TPM-bound LUKS unlock dies every boot&lt;/strong&gt; — do not use &lt;code&gt;--tpm-state=off&lt;/code&gt; / ephemeral auto-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firmware missing&lt;/strong&gt; — install OVMF/AAVMF packages; &lt;code&gt;--firmware=list&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-vmspawn&lt;/code&gt; closes the gap between "nspawn is too weak" and "I guess I will memorize forty QEMU flags."&lt;/p&gt;

&lt;p&gt;Practical defaults for day-to-day labs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-vmspawn &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;./disk.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-user-mode&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm&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;--register&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, add ephemeral snapshots for CI-style smoke tests, TAP when the guest must sit on a real L2 segment, credentials instead of baking secrets into the image, and vsock+SSH when you want a shell without wiring a second NIC.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/systemd-vmspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-vmspawn(1)&lt;/a&gt; — Debian man page (options, examples, exit status)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-vmspawn.1.en" rel="noopener noreferrer"&gt;systemd-vmspawn(1)&lt;/a&gt; — Arch man page (includes newer firmware/coco notes)&lt;/li&gt;
&lt;li&gt;Upstream man source: &lt;a href="https://github.com/systemd/systemd/blob/main/man/systemd-vmspawn.xml" rel="noopener noreferrer"&gt;systemd/systemd &lt;code&gt;man/systemd-vmspawn.xml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/systemd-nspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-nspawn(1)&lt;/a&gt; — namespace sibling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/machinectl.1.en.html" rel="noopener noreferrer"&gt;machinectl(1)&lt;/a&gt; — machine registration / shell&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/systemd-creds.1.en.html" rel="noopener noreferrer"&gt;systemd-creds(1)&lt;/a&gt; — encrypt/list service credentials&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/libsystemd-dev/sd_notify.3.en.html" rel="noopener noreferrer"&gt;sd_notify(3)&lt;/a&gt; — readiness protocol (&lt;code&gt;READY=1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/boot_loader_specification/" rel="noopener noreferrer"&gt;UAPI Boot Loader Specification&lt;/a&gt; — BLS lookup for directory boots&lt;/li&gt;
&lt;li&gt;Related reading on this blog: nspawn containers, sysext/confext, ukify/UKI, portable services, soft-reboot&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Rebuilding the Whole OS Image: Practical systemd-sysext and confext on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 21 Sep 2026 05:02:27 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-rebuilding-the-whole-os-image-practical-systemd-sysext-and-confext-on-linux-2jgb</link>
      <guid>https://dev.to/lyraalishaikh/stop-rebuilding-the-whole-os-image-practical-systemd-sysext-and-confext-on-linux-2jgb</guid>
      <description>&lt;h1&gt;
  
  
  Stop Rebuilding the Whole OS Image: Practical systemd-sysext and confext on Linux
&lt;/h1&gt;

&lt;p&gt;Immutable and image-based Linux hosts are great until you need one more tool on &lt;code&gt;/usr&lt;/code&gt;, a debug binary that was deliberately left out of the base image, or a temporary &lt;code&gt;/etc&lt;/code&gt; overlay for a fleet flag flip.&lt;/p&gt;

&lt;p&gt;Rebuilding the whole OS image for that is slow. Dropping files straight onto a read-only root is either impossible or a recipe for drift. &lt;strong&gt;systemd-sysext&lt;/strong&gt; and &lt;strong&gt;systemd-confext&lt;/strong&gt; solve a narrower problem cleanly: merge extension images into the live hierarchy with OverlayFS, then unmerge them without rewriting the base OS.&lt;/p&gt;

&lt;p&gt;This is not a package manager. It is not a container runtime. And it is not the same thing as portable services.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sysext and confext actually do
&lt;/h2&gt;

&lt;p&gt;Per &lt;code&gt;systemd-sysext(8)&lt;/code&gt; and the &lt;a href="https://uapi-group.org/specifications/specs/extension_image/" rel="noopener noreferrer"&gt;UAPI.4 Extension Images&lt;/a&gt; specification:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Hierarchies extended&lt;/th&gt;
&lt;th&gt;Identity file&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sysext&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usr/&lt;/code&gt; and &lt;code&gt;/opt/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/lib/extension-release.d/extension-release.NAME&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;confext&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/etc/&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/extension-release.d/extension-release.NAME&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you &lt;strong&gt;merge&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;systemd finds installed extension images&lt;/li&gt;
&lt;li&gt;builds an OverlayFS stack over the host hierarchy&lt;/li&gt;
&lt;li&gt;overmounts &lt;code&gt;/usr&lt;/code&gt; + &lt;code&gt;/opt&lt;/code&gt; (sysext) or &lt;code&gt;/etc&lt;/code&gt; (confext)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you &lt;strong&gt;unmerge&lt;/strong&gt;, the overlay goes away and the original host tree is visible again.&lt;/p&gt;

&lt;p&gt;Important constraints from the man page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files outside the supported hierarchies in an image are &lt;strong&gt;ignored&lt;/strong&gt;. A sysext with &lt;code&gt;/etc/foo&lt;/code&gt; will not put anything into host &lt;code&gt;/etc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Extensions are &lt;strong&gt;read-only by default&lt;/strong&gt;. On a mutable host, merging them makes the overlaid base directory read-only unless you enable mutability.&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;no dependency solver&lt;/strong&gt;. An extension must carry what it needs (beyond what the base OS already provides).&lt;/li&gt;
&lt;li&gt;Extensions are supposed to be &lt;strong&gt;additive&lt;/strong&gt;. OverlayFS can shadow or whiteout files, but that is discouraged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  sysext vs portable services (do not mix these up)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;systemd-sysext / confext&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;portablectl portable services&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Goal&lt;/td&gt;
&lt;td&gt;Extend host trees as if files shipped in the OS&lt;/td&gt;
&lt;td&gt;Attach a service image with sandboxing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;None&lt;/strong&gt; — files appear on the host&lt;/td&gt;
&lt;td&gt;Service-level sandbox profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical content&lt;/td&gt;
&lt;td&gt;Tools, libraries, optional &lt;code&gt;/usr&lt;/code&gt; add-ons, &lt;code&gt;/etc&lt;/code&gt; overlays&lt;/td&gt;
&lt;td&gt;Long-running units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;merge&lt;/code&gt; / &lt;code&gt;refresh&lt;/code&gt; / boot services&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;attach&lt;/code&gt; / &lt;code&gt;detach&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are shipping a daemon that should stay sandboxed, prefer portable services. If you need &lt;code&gt;strace&lt;/code&gt;, a locally built &lt;code&gt;libfoo&lt;/code&gt;, or a fleet-wide &lt;code&gt;/etc&lt;/code&gt; toggle on an otherwise immutable image, use sysext/confext.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image formats and search paths
&lt;/h2&gt;

&lt;p&gt;Supported formats match other systemd image tooling (&lt;code&gt;nspawn&lt;/code&gt;, &lt;code&gt;RootImage=&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plain directories or btrfs subvolumes&lt;/li&gt;
&lt;li&gt;GPT disk images following the Discoverable Partitions Spec&lt;/li&gt;
&lt;li&gt;Naked filesystem images (erofs, squashfs, ext4, …) as &lt;code&gt;*.raw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Optional dm-verity authenticity on disk images&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;sysext search paths&lt;/strong&gt; (first useful for symlinks; primary install location last in practice for bulk images):&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; ← primary place for real images&lt;/li&gt;
&lt;li&gt;In the initrd: &lt;code&gt;/.extra/sysext/&lt;/code&gt; (populated by &lt;code&gt;systemd-stub&lt;/code&gt; from ESP companions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;confext search paths&lt;/strong&gt;:&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; ← primary&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;Directories = directory-based extensions. Files ending in &lt;code&gt;.raw&lt;/code&gt; = disk-image extensions.&lt;/p&gt;

&lt;p&gt;There is no per-image enable bit: &lt;strong&gt;everything installed is eligible for merge&lt;/strong&gt;. To mask a lower-precedence image, place an empty directory with the same name under &lt;code&gt;/etc/extensions/&lt;/code&gt; (sysext). Kernel cmdline knobs can disable auto-merge entirely: &lt;code&gt;systemd.sysext=0&lt;/code&gt;, &lt;code&gt;systemd.confext=0&lt;/code&gt; (and &lt;code&gt;rd.*&lt;/code&gt; variants in the initrd).&lt;/p&gt;

&lt;h2&gt;
  
  
  Version matching with extension-release
&lt;/h2&gt;

&lt;p&gt;Every sysext needs:&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;Every confext needs:&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 image/directory name (see &lt;code&gt;os-release(5)&lt;/code&gt; / extension-release rules). Matching against the host &lt;code&gt;os-release&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ID=&lt;/code&gt; must match the host, &lt;strong&gt;or&lt;/strong&gt; be &lt;code&gt;_any&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;ID=&lt;/code&gt; is not &lt;code&gt;_any&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt; (sysext) or &lt;code&gt;CONFEXT_LEVEL=&lt;/code&gt; (confext) must match when present&lt;/li&gt;
&lt;li&gt;otherwise &lt;code&gt;VERSION_ID=&lt;/code&gt; must match&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;ARCHITECTURE=&lt;/code&gt; must match the kernel arch unless &lt;code&gt;_any&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;EXTENSION_RELOAD_MANAGER=1&lt;/code&gt; asks for a manager reload after apply&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; ship &lt;code&gt;/usr/lib/os-release&lt;/code&gt; inside a sysext — it would override host OS identity after merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can force a merge past version checks with &lt;code&gt;--force&lt;/code&gt; (lab only).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab: directory-based sysext in 10 minutes
&lt;/h2&gt;

&lt;p&gt;This lab builds a tiny additive extension that drops a script under &lt;code&gt;/usr/local&lt;/code&gt;… wait: &lt;code&gt;/usr/local&lt;/code&gt; is under &lt;code&gt;/usr&lt;/code&gt;, so it works. Prefer &lt;code&gt;/usr&lt;/code&gt; vendor paths for real images; &lt;code&gt;/opt&lt;/code&gt; is also valid for third-party trees.&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;# Read host identity you must match (unless ID=_any)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(ID|VERSION_ID|SYSEXT_LEVEL)='&lt;/span&gt; /etc/os-release

&lt;span class="c"&gt;# Example: allow any OS ID for a throwaway lab image&lt;/span&gt;
&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"devtools-lab"&lt;/span&gt;
&lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/extensions/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sudo 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;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/extension-release.d"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin"&lt;/span&gt;

&lt;span class="c"&gt;# Identity file NAME must match directory name&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/extension-release.d/extension-release.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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=_any
ARCHITECTURE=_any
# Optional self-description with SYSEXT_ prefix:
SYSEXT_ID=devtools-lab
SYSEXT_VERSION_ID=0.1.0
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Payload: one additive binary/script&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin/sysext-hello"&lt;/span&gt; &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;'
#!/bin/sh
echo "hello from systemd-sysext"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin/sysext-hello"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect and merge:&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
systemd-sysext status

&lt;span class="c"&gt;# First merge (fails if already merged — then use refresh)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext merge
&lt;span class="c"&gt;# After installing/removing images on an already-merged host:&lt;/span&gt;
&lt;span class="c"&gt;# sudo systemd-sysext refresh&lt;/span&gt;

&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; sysext-hello
sysext-hello
&lt;span class="c"&gt;# hello from systemd-sysext&lt;/span&gt;

findmnt /usr
&lt;span class="c"&gt;# expect overlay on /usr when sysext is active&lt;/span&gt;

systemd-sysext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmerge cleanly:&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;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; sysext-hello &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"gone — base OS restored"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  refresh caveats
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;refresh&lt;/code&gt; is unmerge + merge. The man page is explicit: there is a &lt;strong&gt;brief window&lt;/strong&gt; where neither overlay is mounted, so extension files disappear momentarily even if the same extension remains installed. Plan service restarts and long-running readers accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  matching a real distro instead of ID=_any
&lt;/h3&gt;

&lt;p&gt;For production images built beside the base OS:&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;# Example pattern — values must match the target host os-release&lt;/span&gt;
&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian
&lt;span class="nv"&gt;VERSION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;13
&lt;span class="c"&gt;# or, when the distro defines it:&lt;/span&gt;
&lt;span class="c"&gt;# SYSEXT_LEVEL=1.0&lt;/span&gt;
&lt;span class="nv"&gt;ARCHITECTURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;x86-64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture identifiers follow &lt;code&gt;ConditionArchitecture=&lt;/code&gt; style names (&lt;code&gt;x86-64&lt;/code&gt;, &lt;code&gt;arm64&lt;/code&gt;, …), not always raw &lt;code&gt;uname -m&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab: confext for a reversible /etc overlay
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ssh-banner-lab"&lt;/span&gt;
&lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/confexts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sudo 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;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/etc/extension-release.d"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/etc"&lt;/span&gt;

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/etc/extension-release.d/extension-release.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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=_any
ARCHITECTURE=_any
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Additive file preferred. Shadowing existing files works via overlayfs&lt;/span&gt;
&lt;span class="c"&gt;# but is discouraged for general packaging.&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;/etc/ssh/banner.sysext"&lt;/span&gt; &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;'
Authorized access only — confext lab banner
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemd-confext list
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext merge   &lt;span class="c"&gt;# or refresh&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /etc/ssh/banner.sysext
findmnt /etc

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-confext unmerge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confext merges mount &lt;code&gt;/etc&lt;/code&gt; with &lt;strong&gt;&lt;code&gt;nosuid&lt;/code&gt;&lt;/strong&gt; and, by default, &lt;strong&gt;&lt;code&gt;noexec&lt;/code&gt;&lt;/strong&gt; (override with &lt;code&gt;--noexec=false&lt;/code&gt; only if you understand the trade-off).&lt;/p&gt;

&lt;h2&gt;
  
  
  Boot integration
&lt;/h2&gt;

&lt;p&gt;Enable the oneshots/services your distro ships:&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 systemd-sysext.service systemd-confext.service
&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; systemd-sysext.service
&lt;span class="c"&gt;# enable confext only if you use confexts&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; systemd-confext.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are guaranteed to finish &lt;strong&gt;before &lt;code&gt;basic.target&lt;/code&gt;&lt;/strong&gt;, so normal services can rely on merged files under &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/opt&lt;/code&gt;, and &lt;code&gt;/etc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Early-boot / initrd helpers also exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysext-initrd.service&lt;/code&gt; / &lt;code&gt;systemd-confext-initrd.service&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysext-sysroot.service&lt;/code&gt; / &lt;code&gt;systemd-confext-sysroot.service&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sysroot helpers matter when you need extension content visible to very early consumers (for example some &lt;code&gt;systemd-sysusers&lt;/code&gt; definitions). Note the documented limitation: with a split &lt;code&gt;/var&lt;/code&gt;, extensions under &lt;code&gt;/sysroot/var/lib/extensions&lt;/code&gt; may not merge in the sysroot pass and are handled later by the main OS services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutability modes (systemd 256+)
&lt;/h2&gt;

&lt;p&gt;Default merge on a writable host &lt;strong&gt;freezes&lt;/strong&gt; the overlaid base directory as read-only for as long as extensions remain merged. That surprises people on classic package-managed systems.&lt;/p&gt;

&lt;p&gt;Modes from &lt;code&gt;systemd-sysext(8)&lt;/code&gt; / UAPI.4:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no&lt;/code&gt; / disabled&lt;/td&gt;
&lt;td&gt;Always immutable (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mutable only if write-routing paths exist under &lt;code&gt;/var/lib/extensions.mutable/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yes&lt;/code&gt; / enabled&lt;/td&gt;
&lt;td&gt;Force mutable; create routing dirs as needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Immutable overlay, but seed from routing dirs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ephemeral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mutable into temporary upperdirs discarded on unmerge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ephemeral-import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ephemeral + import seed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Write routing (non-ephemeral):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/usr&lt;/code&gt; writes → &lt;code&gt;/var/lib/extensions.mutable/usr/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/opt&lt;/code&gt; writes → &lt;code&gt;/var/lib/extensions.mutable/opt/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc&lt;/code&gt; writes → &lt;code&gt;/var/lib/extensions.mutable/etc/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To keep the real host tree writable while merged, symlink routing targets 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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions.mutable
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; /usr /var/lib/extensions.mutable/usr
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; /opt /var/lib/extensions.mutable/opt
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; /etc /var/lib/extensions.mutable/etc

&lt;span class="c"&gt;# one-shot:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh &lt;span class="nt"&gt;--mutable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto
&lt;span class="c"&gt;# or persist via sysext.conf / confext.conf (Mutable=)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persistent config (see &lt;code&gt;sysext.conf(5)&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;# /etc/systemd/sysext.conf.d/20-mutable.conf&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;SysExt]
&lt;span class="nv"&gt;Mutable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto

&lt;span class="c"&gt;# /etc/systemd/confext.conf.d/20-mutable.conf&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;ConfExt]
&lt;span class="nv"&gt;Mutable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Mutable=&lt;/code&gt; in conf files is a relatively new knob (documented around systemd 259 in current man pages); CLI &lt;code&gt;--mutable=&lt;/code&gt; landed in 256. Check &lt;code&gt;systemd-sysext --version&lt;/code&gt; on your hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disk images, verity, and image policy
&lt;/h2&gt;

&lt;p&gt;Directory trees are perfect for labs. Production immutable fleets usually want &lt;code&gt;*.raw&lt;/code&gt; images (often erofs/squashfs) with optional verity, built in the same pipeline as the base OS (mkosi is a common choice).&lt;/p&gt;

&lt;p&gt;When operating on disk images, systemd enforces an &lt;strong&gt;image policy&lt;/strong&gt; (&lt;code&gt;systemd.image-policy(7)&lt;/code&gt;). Defaults roughly allow root/usr with verity/signed/encrypted/unprotected/absent combinations for sysext; initrd &lt;code&gt;/.extra/sysext/&lt;/code&gt; defaults are stricter (&lt;code&gt;signed&lt;/code&gt; oriented). Override with:&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;span class="nt"&gt;--image-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root=verity+signed:usr=verity+signed'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tighten this deliberately on production appliances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational patterns that work well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Optional debug/toolchain layer on immutable hosts
&lt;/h3&gt;

&lt;p&gt;Ship &lt;code&gt;gdb&lt;/code&gt;, &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;perf&lt;/code&gt;, and friends as a signed sysext. Merge when diagnosing; unmerge when done. Base image stays minimal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Local rebuild of one component
&lt;/h3&gt;

&lt;p&gt;From the man page’s own example 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;make &lt;span class="nv"&gt;DESTDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/extensions/mytest &lt;span class="nb"&gt;install
sudo &lt;/span&gt;systemd-sysext refresh
&lt;span class="c"&gt;# exercise /usr paths as if installed into the OS&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/extensions/mytest
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. confext feature flags
&lt;/h3&gt;

&lt;p&gt;Bake &lt;code&gt;/etc&lt;/code&gt; drop-ins for a service into a confext. Deploy the image, &lt;code&gt;systemd-confext refresh&lt;/code&gt;, restart the unit. Remove the confext to make the old config disappear with the overlay — no leftover drop-in archaeology.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. UKI companion sysext in the ESP
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;systemd-stub&lt;/code&gt; can expose extension images from the ESP into &lt;code&gt;/.extra/sysext/&lt;/code&gt; for initrd/early use. Pair this with UKI workflows when the extension must be available before the real root’s &lt;code&gt;/var&lt;/code&gt; is online.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext list
systemd-sysext status
systemd-confext list
systemd-confext status

findmnt /usr /opt /etc
mount | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'overlay|sysext|confext'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# After merge, confirm additive path and that host identity is intact&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; /usr/lib/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; ^ID&lt;span class="o"&gt;=&lt;/span&gt; /usr/lib/os-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rollback and recovery
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bad extension content&lt;/td&gt;
&lt;td&gt;Remove image from &lt;code&gt;/var/lib/extensions&lt;/code&gt; or &lt;code&gt;/var/lib/confexts&lt;/code&gt;, then &lt;code&gt;refresh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need base tree immediately&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-sysext unmerge&lt;/code&gt; / &lt;code&gt;systemd-confext unmerge&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot loop blamed on extensions&lt;/td&gt;
&lt;td&gt;Kernel cmdline &lt;code&gt;systemd.sysext=0&lt;/code&gt; and/or &lt;code&gt;systemd.confext=0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mask one image without deleting&lt;/td&gt;
&lt;td&gt;Empty directory mask under &lt;code&gt;/etc/extensions/NAME&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accidentally read-only &lt;code&gt;/usr&lt;/code&gt; after merge&lt;/td&gt;
&lt;td&gt;Unmerge, or enable an appropriate &lt;code&gt;--mutable=&lt;/code&gt; mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What not to use this for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replacing apt/dnf/pacman&lt;/strong&gt; — no dependencies, no file conflict database, no updates channel of its own&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Untrusted third-party code&lt;/strong&gt; — zero isolation; treat sysext like installing into the OS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping sandboxed long-running services&lt;/strong&gt; — use &lt;code&gt;portablectl&lt;/code&gt; instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B OS updates&lt;/strong&gt; — that is &lt;code&gt;systemd-sysupdate&lt;/code&gt; / image slots territory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-file integrity of arbitrary mutable paths&lt;/strong&gt; — look at fs-verity / dm-verity designs instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Minimal production recipe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) Build extension tree or .raw in CI alongside the base image&lt;/span&gt;
&lt;span class="c"&gt;# 2) Install to the host&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /var/lib/extensions
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;dist/devtools.raw /var/lib/extensions/devtools.raw
&lt;span class="c"&gt;# directory form also fine:&lt;/span&gt;
&lt;span class="c"&gt;# sudo rsync -a dist/devtools/ /var/lib/extensions/devtools/&lt;/span&gt;

&lt;span class="c"&gt;# 3) Merge now and on boot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;systemd-sysext.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh

&lt;span class="c"&gt;# 4) Confirm&lt;/span&gt;
systemd-sysext status
findmnt /usr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For confext, mirror the same flow under &lt;code&gt;/var/lib/confexts/&lt;/code&gt; and &lt;code&gt;systemd-confext&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-sysext.8.en" rel="noopener noreferrer"&gt;systemd-sysext(8)&lt;/a&gt; — merge/unmerge/refresh, mutability, search paths, initrd/sysroot services&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/sysext.conf.5.en" rel="noopener noreferrer"&gt;sysext.conf(5)&lt;/a&gt; — &lt;code&gt;Mutable=&lt;/code&gt;, &lt;code&gt;ImagePolicy=&lt;/code&gt; drop-ins&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/os-release.5.en" rel="noopener noreferrer"&gt;os-release(5) / extension-release&lt;/a&gt; — identity and matching rules&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/extension_image/" rel="noopener noreferrer"&gt;UAPI.4 Extension Images&lt;/a&gt; — sysext vs confext, ordering, mutability model&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd.image-policy.7.en" rel="noopener noreferrer"&gt;systemd.image-policy(7)&lt;/a&gt; — disk image admission policy&lt;/li&gt;
&lt;li&gt;Portable Services documentation (linked from the sysext man page) — isolation contrast&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-sysext&lt;/code&gt; and &lt;code&gt;systemd-confext&lt;/code&gt; give you a first-class, reversible way to layer files onto &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/opt&lt;/code&gt;, and &lt;code&gt;/etc&lt;/code&gt; without pretending to be a package manager or a container. Keep extensions additive, match &lt;code&gt;extension-release&lt;/code&gt; carefully, enable mutability only when you mean to, and pick portable services when you need isolation instead of a host-tree merge.&lt;/p&gt;

&lt;p&gt;Once you have a base image you trust, most “just one more binary” problems stop being full rebuilds — they become an extension refresh.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Assembling Boot Pieces by Hand: Practical Unified Kernel Images with ukify on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 20 Sep 2026 05:03:54 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-assembling-boot-pieces-by-hand-practical-unified-kernel-images-with-ukify-on-linux-522b</link>
      <guid>https://dev.to/lyraalishaikh/stop-assembling-boot-pieces-by-hand-practical-unified-kernel-images-with-ukify-on-linux-522b</guid>
      <description>&lt;h1&gt;
  
  
  Stop Assembling Boot Pieces by Hand: Practical Unified Kernel Images with ukify on Linux
&lt;/h1&gt;

&lt;p&gt;Most Linux boots still treat the kernel, initrd, command line, and splash as separate moving parts. That works — until you want one signed PE binary the firmware or boot loader can trust as a whole.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Unified Kernel Image (UKI)&lt;/strong&gt; packs those pieces into a single UEFI PE/COFF application. &lt;strong&gt;ukify&lt;/strong&gt; is the recommended builder: it wraps a kernel and initrd with &lt;code&gt;systemd-stub&lt;/code&gt;, embeds metadata sections, can Secure Boot-sign the result, and can pre-calculate TPM2 PCR 11 policies via &lt;code&gt;systemd-measure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is the builder-side companion to boot-manager work with &lt;code&gt;bootctl&lt;/code&gt;. Here the focus is constructing Type #2 UKIs correctly, inspecting them, signing them, and wiring &lt;code&gt;kernel-install&lt;/code&gt; so package updates keep producing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a UKI actually is
&lt;/h2&gt;

&lt;p&gt;Per the &lt;a href="https://uapi-group.org/specifications/specs/unified_kernel_image/" rel="noopener noreferrer"&gt;UAPI.5 Unified Kernel Image specification&lt;/a&gt;, a UKI is a PE/COFF UEFI application (&lt;code&gt;IMAGE_SUBSYSTEM_EFI_APPLICATION&lt;/code&gt;) that embeds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PE section&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stub (&lt;code&gt;.text&lt;/code&gt;/… )&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-stub&lt;/code&gt; UEFI entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.linux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ELF kernel image (&lt;strong&gt;required&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.osrel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;os-release&lt;/code&gt; contents for menus/versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.cmdline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kernel command line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.initrd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initramfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.ucode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Microcode initrd (uncompressed; first)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.splash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BMP splash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.dtb&lt;/code&gt; / &lt;code&gt;.dtbauto&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DeviceTree (fixed or auto-matched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.hwids&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hardware ID table for auto DTB/firmware match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.uname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uname -r&lt;/code&gt; string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.sbat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shim SBAT revocation metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.pcrsig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signed expected TPM2 PCR 11 values (JSON)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.pcrpkey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PEM public key matching those signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;systemd-stub&lt;/code&gt; loads the embedded sections, measures most of them into &lt;strong&gt;TPM PCR 11&lt;/strong&gt;, optionally collects companion files next to the UKI, then boots the kernel. Because Secure Boot signs the PE as a whole, kernel + initrd + cmdline travel under one trust decision.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; assemble this with &lt;code&gt;objcopy&lt;/code&gt;. &lt;strong&gt;Don't.&lt;/strong&gt; Section alignment, SBAT merges, Secure Boot signing, and PCR measurement are easy to get subtly wrong. Use &lt;code&gt;ukify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the builder
&lt;/h2&gt;

&lt;p&gt;On Debian 13 / Ubuntu with systemd 257-era packages:&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;systemd-ukify systemd-boot-efi sbsigntool

&lt;span class="c"&gt;# Optional but useful&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-boot binutils  &lt;span class="c"&gt;# bootctl + objdump helpers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemd-ukify&lt;/code&gt; pulls in &lt;code&gt;python3-pefile&lt;/code&gt; (and friends). The EFI stub lives under:&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;ls&lt;/span&gt; /usr/lib/systemd/boot/efi/
&lt;span class="c"&gt;# linuxx64.efi.stub  linuxia32.efi.stub  linuxaa64.efi.stub  …&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify &lt;span class="nt"&gt;--version&lt;/span&gt;
ukify &lt;span class="nt"&gt;--help&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verbs you will use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ukify build&lt;/code&gt; — assemble a UKI (or addon)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ukify genkey&lt;/code&gt; — create Secure Boot + PCR key material from config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ukify inspect&lt;/code&gt; — dump sections, sizes, digests, text payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab: build a minimal unsigned UKI
&lt;/h2&gt;

&lt;p&gt;Work in a throwaway directory. Paths below match a typical Debian layout; adjust &lt;code&gt;KERNEL_VER&lt;/code&gt; to your running kernel.&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;KERNEL_VER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/uki-lab"&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;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Kernel image (Debian/Ubuntu usually ship vmlinuz under /boot as well)&lt;/span&gt;
&lt;span class="nv"&gt;LINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/lib/modules/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KERNEL_VER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/vmlinuz"&lt;/span&gt;
&lt;span class="c"&gt;# Fallback if your distro only keeps it in /boot:&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;LINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/boot/vmlinuz-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KERNEL_VER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Existing initrd from the package/initramfs tools&lt;/span&gt;
&lt;span class="nv"&gt;INITRD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/boot/initrd.img-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KERNEL_VER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# Some systems use initramfs-*.img — pick what exists:&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;INITRD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/boot/initramfs-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KERNEL_VER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.img"&lt;/span&gt;

&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--os-release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@&lt;span class="s2"&gt;"/etc/os-release"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KERNEL_VER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/minimal.unsigned.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you omit &lt;code&gt;--output=&lt;/code&gt;, ukify names the file after the kernel with a &lt;code&gt;.unsigned.efi&lt;/code&gt; or &lt;code&gt;.signed.efi&lt;/code&gt; suffix depending on whether Secure Boot signing ran.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect what you built
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify inspect minimal.unsigned.efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see well-known sections (at least &lt;code&gt;.linux&lt;/code&gt;, and whatever you embedded). Dig into one section:&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;# Print .cmdline as text&lt;/span&gt;
ukify inspect minimal.unsigned.efi &lt;span class="nt"&gt;--section&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.cmdline:text

&lt;span class="c"&gt;# JSON summary (ukify ≥255)&lt;/span&gt;
ukify inspect minimal.unsigned.efi &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 80

&lt;span class="c"&gt;# PE headers via llvm/binutils if installed&lt;/span&gt;
llvm-objdump &lt;span class="nt"&gt;-p&lt;/span&gt; minimal.unsigned.efi 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;span class="c"&gt;# or:&lt;/span&gt;
objdump &lt;span class="nt"&gt;-p&lt;/span&gt; minimal.unsigned.efi | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bootctl&lt;/code&gt; and &lt;code&gt;kernel-install&lt;/code&gt; recognize UKIs partly via &lt;code&gt;.osrel&lt;/code&gt; and PE layout — leaving &lt;code&gt;.osrel&lt;/code&gt; empty is allowed by ukify but &lt;strong&gt;not recommended&lt;/strong&gt;, because other tools may stop treating the artifact as a UKI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config file workflow (preferred for hosts)
&lt;/h2&gt;

&lt;p&gt;Command-line flags are fine for labs. On real machines, put policy in a config file. ukify loads the first of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;--config=PATH&lt;/code&gt; (explicit)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/systemd/ukify.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/systemd/ukify.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/systemd/ukify.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/systemd/ukify.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;kernel-install&lt;/code&gt;'s ukify plugin path conventionally uses &lt;strong&gt;&lt;code&gt;/etc/kernel/uki.conf&lt;/code&gt;&lt;/strong&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 tee&lt;/span&gt; /etc/kernel/uki.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;'
[UKI]
Cmdline=quiet rw
OSRelease=@/etc/os-release
# SecureBootPrivateKey=/etc/kernel/secure-boot-key.pem
# SecureBootCertificate=/etc/kernel/secure-boot-certificate.pem
# SignKernel=yes
# PCRBanks=sha256

# [PCRSignature:initrd]
# Phases=enter-initrd
# PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-initrd.pem
# PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-initrd.pem

# [PCRSignature:system]
# Phases=enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit enter-initrd:leave-initrd:sysinit:ready
# PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-system.pem
# PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-system.pem
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build with that policy while still passing the kernel/initrd on the CLI (they change every upgrade):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/kernel/uki.conf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KERNEL_VER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/host-style.unsigned.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--summary&lt;/code&gt; is excellent for debugging merge order (config vs CLI):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/kernel/uki.conf &lt;span class="nt"&gt;--summary&lt;/span&gt; build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Secure Boot signing with &lt;code&gt;ukify genkey&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If the target machine verifies PE signatures (or you enroll your own keys with &lt;code&gt;sbctl&lt;/code&gt; / firmware Setup Mode), sign the UKI as a whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Decide key paths in config
&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/kernel/uki.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;'
[UKI]
Cmdline=quiet rw
OSRelease=@/etc/os-release
SecureBootPrivateKey=/etc/kernel/secure-boot-key.pem
SecureBootCertificate=/etc/kernel/secure-boot-certificate.pem
SignKernel=yes
SecureBootCertificateValidity=3650
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Generate keys once
&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;# Files must not already exist&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ukify genkey &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/kernel/uki.conf
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/kernel/secure-boot-key.pem
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/kernel/secure-boot-certificate.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;genkey&lt;/code&gt; writes whatever private/public material the config declares (Secure Boot cert/key and any PCR key pairs). Certificate validity defaults to &lt;strong&gt;3650 days&lt;/strong&gt; (10 years) unless you override &lt;code&gt;SecureBootCertificateValidity=&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Build a signed UKI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/kernel/uki.conf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KERNEL_VER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/minimal.signed.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signing tool selection (&lt;code&gt;SecureBootSigningTool=&lt;/code&gt; / &lt;code&gt;--signtool=&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sbsign&lt;/code&gt; (default) — needs &lt;code&gt;sbsigntool&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pesign&lt;/code&gt; — NSS cert DB (&lt;code&gt;SecureBootCertificateDir=&lt;/code&gt;, &lt;code&gt;SecureBootCertificateName=&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-sbsign&lt;/code&gt; — supports OpenSSL providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Boundary vs &lt;code&gt;sbctl&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;sbctl&lt;/code&gt; owns platform key enrollment (PK/KEK/db) and re-signing hooks for arbitrary EFI binaries. &lt;code&gt;ukify&lt;/code&gt; owns &lt;em&gt;UKI assembly&lt;/em&gt; and can call a signtool for the resulting PE. Use both: enroll with &lt;code&gt;sbctl&lt;/code&gt;, build/sign UKIs with &lt;code&gt;ukify&lt;/code&gt; (or sign the ukify output with &lt;code&gt;sbctl sign&lt;/code&gt; if that is your house style).&lt;/p&gt;

&lt;p&gt;With Secure Boot enabled, if a &lt;code&gt;.cmdline&lt;/code&gt; section is present, firmware/stub &lt;strong&gt;ignores&lt;/strong&gt; attempt to override the command line at invocation time. That is intentional: the signed cmdline is part of the trusted image. Want local overrides? Omit &lt;code&gt;.cmdline&lt;/code&gt; from the UKI, or ship signed &lt;strong&gt;addons&lt;/strong&gt; (below).&lt;/p&gt;

&lt;h2&gt;
  
  
  PCR 11 measurement and signed policies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-stub&lt;/code&gt; measures UKI sections into &lt;strong&gt;PCR 11&lt;/strong&gt; (the &lt;code&gt;.pcrsig&lt;/code&gt; section itself is excluded so signatures are not circular). &lt;code&gt;systemd-measure&lt;/code&gt; pre-calculates those values the same way the stub will, optionally signs them, and &lt;code&gt;ukify&lt;/code&gt; embeds the JSON into &lt;code&gt;.pcrsig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Typical goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unlock LUKS only for kernels you signed (&lt;code&gt;systemd-cryptenroll&lt;/code&gt; TPM2 policies)&lt;/li&gt;
&lt;li&gt;Unlock &lt;code&gt;systemd-creds&lt;/code&gt; encrypted credentials only for known UKIs&lt;/li&gt;
&lt;li&gt;Bind secrets to boot &lt;em&gt;phases&lt;/em&gt; (initrd vs multi-user)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default phase paths used by &lt;code&gt;systemd-measure&lt;/code&gt; when you do not pass &lt;code&gt;--phases=&lt;/code&gt; / &lt;code&gt;Phases=&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enter-initrd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enter-initrd:leave-initrd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enter-initrd:leave-initrd:sysinit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enter-initrd:leave-initrd:sysinit:ready&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: separate keys for initrd-only secrets vs runtime secrets (from the ukify manual’s “bells and whistles” pattern):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pcr-private-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2-pcr-private-key-initrd.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pcr-public-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2-pcr-public-key-initrd.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--phases&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'enter-initrd'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pcr-private-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2-pcr-private-key-system.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pcr-public-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2-pcr-public-key-system.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--phases&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit enter-initrd:leave-initrd:sysinit:ready'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pcr-banks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sha256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/measured.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the command line, each &lt;code&gt;--pcr-private-key=&lt;/code&gt; pairs with the matching &lt;code&gt;--phases=&lt;/code&gt; in order. In config files, group them under &lt;code&gt;[PCRSignature:NAME]&lt;/code&gt; sections instead.&lt;/p&gt;

&lt;p&gt;Useful related flags:&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;# Print calculated PCR values while building&lt;/span&gt;
ukify build ... &lt;span class="nt"&gt;--measure&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;out.efi

&lt;span class="c"&gt;# Compare expectation vs the live TPM after boot (on systems with a TPM)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/lib/systemd/systemd-measure status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After boot, stub-provided signature material is commonly exposed under &lt;code&gt;/run/systemd/tpm2-pcr-signature.json&lt;/code&gt; and &lt;code&gt;/run/systemd/tpm2-pcr-public-key.pem&lt;/code&gt; (via a synthetic initrd + tmpfiles). &lt;code&gt;systemd-cryptsetup&lt;/code&gt;, &lt;code&gt;systemd-cryptenroll&lt;/code&gt;, and &lt;code&gt;systemd-creds&lt;/code&gt; look there automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;systemd-measure&lt;/code&gt; is still marked experimental in its man page; prefer going through &lt;code&gt;ukify&lt;/code&gt; rather than hand-rolling JSON into PE sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microcode and multiple initrds
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Initrd=&lt;/code&gt; / &lt;code&gt;--initrd=&lt;/code&gt; may be repeated. ukify concatenates them into one &lt;code&gt;.initrd&lt;/code&gt; section in order. Microcode can also use the dedicated &lt;code&gt;.ucode&lt;/code&gt; section (&lt;code&gt;Microcode=&lt;/code&gt; / &lt;code&gt;--microcode=&lt;/code&gt;), which the stub hands to the kernel &lt;strong&gt;before&lt;/strong&gt; other initrds and which must be uncompressed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--microcode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/boot/intel-ucode.img &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/with-ucode.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or early+late initrds without &lt;code&gt;.ucode&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;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/boot/early_cpio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/concat-initrd.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Command-line PE addons
&lt;/h2&gt;

&lt;p&gt;Sometimes you want a &lt;em&gt;signed&lt;/em&gt; extra cmdline (or other UKI-like auxiliary PE) without rebuilding the base UKI. &lt;code&gt;systemd-stub&lt;/code&gt; loads &lt;code&gt;*.addon.efi&lt;/code&gt; companions from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ESP/.../foo.efi.extra.d/*.addon.efi&lt;/code&gt; next to &lt;code&gt;foo.efi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ESP/loader/addons/*.addon.efi&lt;/code&gt; globally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'debug systemd.log_level=debug'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sbat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
uki-addon.example,1,Example addon,uki-addon.example,1,https://example.local'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/debug.addon.efi"&lt;/span&gt;

&lt;span class="c"&gt;# Optionally sign with the same Secure Boot key as the UKI:&lt;/span&gt;
&lt;span class="c"&gt;#   --secureboot-private-key=... --secureboot-certificate=...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Addons are PE binaries that are &lt;strong&gt;not&lt;/strong&gt; full bootable UKIs; they carry auxiliary sections the stub merges at boot. Sign them if Secure Boot is on, or the stub will ignore untrusted companions on locked platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-profile UKIs (quick tour)
&lt;/h2&gt;

&lt;p&gt;Since systemd 257, one PE can carry multiple profiles separated by &lt;code&gt;.profile&lt;/code&gt; sections. Build profile fragments, then join them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ukify build &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'TITLE=Base&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;ID=base'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;profile0.efi

ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'TITLE=Storage Target Mode&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;ID=storagetm'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw rd.systemd.unit=storage-target-mode.target'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;profile1.efi

ukify build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--linux&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initrd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmdline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'quiet rw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--join-profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;profile0.efi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--join-profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;profile1.efi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/multi-profile.efi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boot loaders that understand multi-profile UKIs can present each &lt;code&gt;TITLE=&lt;/code&gt; / &lt;code&gt;ID=&lt;/code&gt; as a selectable entry without storing multiple full kernels. PCR measurement covers the selected profile (plus base sections not overridden).&lt;/p&gt;

&lt;h2&gt;
  
  
  Install with &lt;code&gt;kernel-install&lt;/code&gt; layout=uki
&lt;/h2&gt;

&lt;p&gt;Manual &lt;code&gt;cp&lt;/code&gt; to the ESP works for experiments. For upgrades, teach &lt;code&gt;kernel-install&lt;/code&gt; to install Type #2 UKIs under &lt;code&gt;$BOOT/EFI/Linux/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inspect current detection:&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;kernel-install inspect
&lt;span class="c"&gt;# Look for KERNEL_INSTALL_LAYOUT, BOOT, ENTRY_TOKEN, machine-id, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure layout (and optional generators) in &lt;code&gt;/etc/kernel/install.conf&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; /etc/kernel/install.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;'
layout=uki
# uki_generator=ukify
# initrd_generator=dracut   # or mkinitcpio / the distro default
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the stock plugins do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;layout&lt;/th&gt;
&lt;th&gt;Behavior on &lt;code&gt;add&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;90-loaderentry.install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type #1 entry under &lt;code&gt;$BOOT/loader/entries/&lt;/code&gt; + &lt;code&gt;linux&lt;/code&gt;/initrd files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;90-uki-copy.install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uki&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copies staged &lt;code&gt;uki.efi&lt;/code&gt; (or a &lt;code&gt;.efi&lt;/code&gt; kernel argument) to &lt;code&gt;$BOOT/EFI/Linux/ENTRY-TOKEN-KERNEL-VERSION.efi&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;layout=uki&lt;/code&gt;, after your UKI generator stages &lt;code&gt;$KERNEL_INSTALL_STAGING_AREA/uki.efi&lt;/code&gt;, &lt;code&gt;90-uki-copy.install&lt;/code&gt; places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$BOOT/EFI/Linux/&amp;lt;entry-token&amp;gt;-&amp;lt;kernel-version&amp;gt;.efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$BOOT&lt;/code&gt; is discovered as the first among &lt;code&gt;/efi/&lt;/code&gt;, &lt;code&gt;/boot/&lt;/code&gt;, &lt;code&gt;/boot/efi/&lt;/code&gt; that already looks like a BLS tree. Prefer mounting the ESP on &lt;code&gt;/efi&lt;/code&gt; when you can.&lt;/p&gt;

&lt;p&gt;Install one kernel version explicitly:&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;# After building/staging a UKI for this version — distro plugins vary&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;kernel-install add &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KERNEL_VER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINUX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INITRD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Or list / remove&lt;/span&gt;
kernel-install list
&lt;span class="nb"&gt;sudo &lt;/span&gt;kernel-install remove &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KERNEL_VER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bootctl list&lt;/code&gt; should then show the Type #2 entry. Selecting it boots the PE directly; no separate initrd path in a Type #1 conf file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical packaging note:&lt;/strong&gt; On Debian, &lt;code&gt;systemd-ukify&lt;/code&gt; provides the builder; your image/initrd generator plugin must actually call &lt;code&gt;ukify&lt;/code&gt; and leave &lt;code&gt;uki.efi&lt;/code&gt; in the staging area. If &lt;code&gt;uki_generator=ukify&lt;/code&gt; is not wired on your distro version, keep a small &lt;code&gt;/etc/kernel/install.d/60-ukify.install&lt;/code&gt; that builds into &lt;code&gt;"$KERNEL_INSTALL_STAGING_AREA/uki.efi"&lt;/code&gt; using &lt;code&gt;/etc/kernel/uki.conf&lt;/code&gt;, then let &lt;code&gt;90-uki-copy.install&lt;/code&gt; finish the job. Return &lt;code&gt;0&lt;/code&gt; on success; return &lt;code&gt;77&lt;/code&gt; only if you intend to abort the entire &lt;code&gt;kernel-install&lt;/code&gt; run.&lt;/p&gt;

&lt;h2&gt;
  
  
  End-to-end checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; &lt;code&gt;systemd-ukify&lt;/code&gt;, EFI stub package, and a signtool if you need Secure Boot PE signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; &lt;code&gt;/etc/kernel/uki.conf&lt;/code&gt; with cmdline, os-release, and optional SB/PCR keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ukify genkey --config=...&lt;/code&gt;&lt;/strong&gt; once; lock down private key modes (&lt;code&gt;0600&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; a lab UKI; &lt;strong&gt;&lt;code&gt;ukify inspect&lt;/code&gt;&lt;/strong&gt; sections and digests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enroll&lt;/strong&gt; the Secure Boot certificate into the platform db (via &lt;code&gt;sbctl&lt;/code&gt; or firmware) if verification is enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set&lt;/strong&gt; &lt;code&gt;layout=uki&lt;/code&gt; in &lt;code&gt;/etc/kernel/install.conf&lt;/code&gt; and verify with &lt;code&gt;kernel-install inspect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reinstall&lt;/strong&gt; a kernel package or run &lt;code&gt;kernel-install add&lt;/code&gt; and confirm &lt;code&gt;$BOOT/EFI/Linux/*.efi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bootctl list&lt;/code&gt;&lt;/strong&gt; / reboot into the UKI; on TPM hosts compare &lt;code&gt;systemd-measure status&lt;/code&gt; with build-time &lt;code&gt;--measure&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then&lt;/strong&gt; enroll LUKS/creds policies against the PCR public key.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Operational pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing stub package:&lt;/strong&gt; ukify needs &lt;code&gt;linuxx64.efi.stub&lt;/code&gt; (or arch equivalent) from &lt;code&gt;systemd-boot-efi&lt;/code&gt; / distro equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong initrd path after upgrade:&lt;/strong&gt; always take initrd from the same kernel version you embed; UKIs freeze that pairing on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Boot + embedded cmdline:&lt;/strong&gt; local &lt;code&gt;BootNext&lt;/code&gt; cmdline overrides are ignored; use addons or rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PCR bank mismatch:&lt;/strong&gt; if the OS policy disables SHA-1 signatures, restrict &lt;code&gt;PCRBanks=&lt;/code&gt; / &lt;code&gt;--pcr-banks=&lt;/code&gt; to &lt;code&gt;sha256&lt;/code&gt; (and whatever your TPM actually supports).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty &lt;code&gt;.osrel&lt;/code&gt;:&lt;/strong&gt; other tools may not classify the PE as a UKI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing keys on the build host:&lt;/strong&gt; treat PCR and Secure Boot private keys like production CA material; split initrd vs runtime PCR keys if unlock policies differ by phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Companion files:&lt;/strong&gt; &lt;code&gt;.cred&lt;/code&gt; / &lt;code&gt;.sysext.raw&lt;/code&gt; / &lt;code&gt;.confext.raw&lt;/code&gt; next to the UKI are measured into PCR &lt;strong&gt;12&lt;/strong&gt; and only accepted when authentic under Secure Boot policy — do not treat the ESP drop-in folder as a free-form unsigned config dump on locked systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Covered elsewhere / out of scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bootctl&lt;/code&gt; install, &lt;code&gt;loader.conf&lt;/code&gt;, BLS entry lifecycle&lt;/td&gt;
&lt;td&gt;Boot manager ops (Type #1 vs consuming Type #2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sbctl&lt;/code&gt; PK/KEK/db enrollment&lt;/td&gt;
&lt;td&gt;Platform Secure Boot key ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd-sysupdate&lt;/code&gt; A/B image pulls&lt;/td&gt;
&lt;td&gt;Shipping whole OS slots that &lt;em&gt;may include&lt;/em&gt; UKIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd-cryptenroll&lt;/code&gt; TPM2 unlock UX&lt;/td&gt;
&lt;td&gt;Consuming &lt;code&gt;.pcrsig&lt;/code&gt; after the UKI exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRUB &lt;code&gt;linux&lt;/code&gt;/&lt;code&gt;initrd&lt;/code&gt; snippets&lt;/td&gt;
&lt;td&gt;Non-UKI boot path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distro-custom signed kernel packages&lt;/td&gt;
&lt;td&gt;Vendor-signed vmlinuz without a UKI wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;UKIs turn “kernel + initrd + cmdline + metadata” into one PE you can sign, measure, and install like any other EFI binary. &lt;strong&gt;ukify&lt;/strong&gt; is the practical assembly line: &lt;code&gt;build&lt;/code&gt; for images, &lt;code&gt;genkey&lt;/code&gt; for key material, &lt;code&gt;inspect&lt;/code&gt; for verification, with &lt;code&gt;systemd-measure&lt;/code&gt; handling PCR 11 policy blobs and &lt;code&gt;kernel-install layout=uki&lt;/code&gt; dropping results into &lt;code&gt;$BOOT/EFI/Linux/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Start unsigned in a lab directory, inspect every section, then add Secure Boot signing and PCR signatures once the shape is right. After that, every kernel upgrade can produce the same artifact shape instead of another pile of loosely coupled boot files.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/trixie/systemd-ukify/ukify.1.en.html" rel="noopener noreferrer"&gt;ukify(1) — Debian trixie man page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/ukify.1.html" rel="noopener noreferrer"&gt;ukify(1) — man7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/systemd-stub.7.html" rel="noopener noreferrer"&gt;systemd-stub(7)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/systemd-measure.1.html" rel="noopener noreferrer"&gt;systemd-measure(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/kernel-install.8.html" rel="noopener noreferrer"&gt;kernel-install(8)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://uapi-group.org/specifications/specs/unified_kernel_image/" rel="noopener noreferrer"&gt;UAPI.5 Unified Kernel Image specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/boot_loader_specification/" rel="noopener noreferrer"&gt;UAPI Boot Loader Specification&lt;/a&gt; (Type #1 text entries vs Type #2 UKIs)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rhboot/shim/blob/main/SBAT.md" rel="noopener noreferrer"&gt;Shim SBAT documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Debian package metadata: &lt;code&gt;systemd-ukify&lt;/code&gt; 257.x (&lt;code&gt;apt-cache show systemd-ukify&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Fighting GRUB Menus: Practical systemd-boot with bootctl on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 19 Sep 2026 05:03:18 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-fighting-grub-menus-practical-systemd-boot-with-bootctl-on-linux-2jhl</link>
      <guid>https://dev.to/lyraalishaikh/stop-fighting-grub-menus-practical-systemd-boot-with-bootctl-on-linux-2jhl</guid>
      <description>&lt;h1&gt;
  
  
  Stop Fighting GRUB Menus: Practical systemd-boot with bootctl on Linux
&lt;/h1&gt;

&lt;p&gt;If your boot story still starts with a hand-edited GRUB config, a mystery ESP path, and “I hope the new kernel shows up,” it is time to switch mental models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;systemd-boot&lt;/strong&gt; (sd-boot) is a small UEFI boot manager. It does not try to be a full OS. It reads standard Boot Loader Specification entries from the ESP (and optional XBOOTLDR partition), shows a menu, and hands control to a kernel or UKI. Day-to-day management is done with &lt;strong&gt;bootctl&lt;/strong&gt; from the running OS—not by grepping shell scripts under &lt;code&gt;/etc/grub.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is operational: install, inspect, set defaults, wire kernel packages through &lt;code&gt;kernel-install&lt;/code&gt;, tune &lt;code&gt;loader.conf&lt;/code&gt;, enable early entropy, and use boot counting so a bad kernel does not trap you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope note:&lt;/strong&gt; This is about the &lt;em&gt;boot manager and entry lifecycle&lt;/em&gt;. It is not a Secure Boot key enrollment guide (use &lt;code&gt;sbctl&lt;/code&gt; / firmware tools for that), not A/B image update policy (&lt;code&gt;systemd-sysupdate&lt;/code&gt;), and not userspace-only restart (&lt;code&gt;systemctl soft-reboot&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UEFI firmware (systemd-boot is UEFI-only)&lt;/li&gt;
&lt;li&gt;An ESP (GPT type &lt;code&gt;C12A7328-F81F-11D2-BA4B-00A0C93EC93B&lt;/code&gt;), typically VFAT&lt;/li&gt;
&lt;li&gt;Optional but useful: an Extended Boot Loader partition (XBOOTLDR, GPT type &lt;code&gt;BC13C2FF-59E6-4262-A352-B275FD6F7172&lt;/code&gt;) on the same disk&lt;/li&gt;
&lt;li&gt;Packages (Debian/Ubuntu naming):

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-boot&lt;/code&gt; — integration/services&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-boot-efi&lt;/code&gt; — EFI binaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-boot-tools&lt;/code&gt; — &lt;code&gt;bootctl&lt;/code&gt; and related tools
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu-style&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-boot systemd-boot-efi systemd-boot-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Fedora/RHEL-family systems the bits usually ship closer to the main &lt;code&gt;systemd&lt;/code&gt;/&lt;code&gt;systemd-boot&lt;/code&gt; packages; the commands below are the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model: ESP, $BOOT, Type #1 vs Type #2
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://uapi-group.org/specifications/specs/boot_loader_specification/" rel="noopener noreferrer"&gt;UAPI Boot Loader Specification&lt;/a&gt; defines two entry styles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$BOOT/loader/entries/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text snippets pointing at &lt;code&gt;linux&lt;/code&gt; + &lt;code&gt;initrd&lt;/code&gt; (or &lt;code&gt;efi&lt;/code&gt; / &lt;code&gt;uki&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;#2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$BOOT/EFI/Linux/*.efi&lt;/code&gt; (and ESP)&lt;/td&gt;
&lt;td&gt;Unified Kernel Images: one PE binary with stub + kernel + initrd + metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;$BOOT&lt;/strong&gt; is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XBOOTLDR if it exists&lt;/li&gt;
&lt;li&gt;otherwise the ESP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended mounts (from the BLS):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount &lt;strong&gt;$BOOT&lt;/strong&gt; at &lt;code&gt;/boot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If ESP and XBOOTLDR are both present, mount the &lt;strong&gt;ESP&lt;/strong&gt; at &lt;code&gt;/efi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Avoid nesting ESP under &lt;code&gt;/boot/efi&lt;/code&gt; when you can—it complicates automount setups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;bootctl&lt;/code&gt; discovers these paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bootctl status
bootctl &lt;span class="nt"&gt;--print-esp-path&lt;/span&gt;
bootctl &lt;span class="nt"&gt;--print-boot-path&lt;/span&gt;   &lt;span class="c"&gt;# XBOOTLDR if present, else ESP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kernel-install&lt;/code&gt; uses the same discovery order for installing kernels into &lt;code&gt;$BOOT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect before you touch anything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bootctl status
bootctl list
bootctl is-installed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful things &lt;code&gt;status&lt;/code&gt; reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firmware / Secure Boot state (as the loader sees it)&lt;/li&gt;
&lt;li&gt;Which loader booted you&lt;/li&gt;
&lt;li&gt;ESP / boot partition paths&lt;/li&gt;
&lt;li&gt;Current default / oneshot / selected entry IDs&lt;/li&gt;
&lt;li&gt;Feature flags the loader advertised via EFI variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;list&lt;/code&gt; shows Boot Loader Spec entries plus other discovered options (Windows, firmware setup, EFI shell when present).&lt;/p&gt;

&lt;h2&gt;
  
  
  Install or update systemd-boot on the ESP
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First install: copy sd-boot into the ESP and register it with firmware&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Later: refresh installed copies when package binaries are newer&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl update

&lt;span class="c"&gt;# Confirm&lt;/span&gt;
bootctl is-installed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;install&lt;/code&gt; does (from &lt;code&gt;bootctl(1)&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs the systemd-boot EFI binary into the ESP (including the removable-path fallback &lt;code&gt;EFI/BOOT/BOOT*.EFI&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Adds the loader to the firmware boot order (unless you pass &lt;code&gt;--no-variables&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Signed-file note: if a &lt;code&gt;*.efi.signed&lt;/code&gt; sibling exists, &lt;code&gt;install&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; prefer it—useful when your distro ships pre-signed bootloader binaries for Secure Boot.&lt;/p&gt;

&lt;p&gt;Graceful hosts (images, chroots, odd firmware):&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;bootctl update &lt;span class="nt"&gt;--graceful&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ignores some failure modes (missing ESP write, foreign loader already present) instead of hard-failing.&lt;/p&gt;

&lt;p&gt;To remove systemd-boot from the ESP and firmware list:&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;bootctl remove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the menu: loader.conf
&lt;/h2&gt;

&lt;p&gt;Create or edit &lt;code&gt;$ESP/loader/loader.conf&lt;/code&gt; (often &lt;code&gt;/efi/loader/loader.conf&lt;/code&gt; or &lt;code&gt;/boot/loader/loader.conf&lt;/code&gt; depending on layout):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /efi/loader/loader.conf
&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;default&lt;/span&gt; @&lt;span class="n"&gt;saved&lt;/span&gt;
&lt;span class="n"&gt;editor&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;console&lt;/span&gt;-&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;-&lt;span class="n"&gt;seed&lt;/span&gt;-&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt;-&lt;span class="n"&gt;system&lt;/span&gt;-&lt;span class="n"&gt;token&lt;/span&gt;
&lt;span class="n"&gt;auto&lt;/span&gt;-&lt;span class="n"&gt;firmware&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key knobs from &lt;code&gt;loader.conf(5)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Practical meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds before default boots. &lt;code&gt;0&lt;/code&gt; / &lt;code&gt;menu-hidden&lt;/code&gt; = no menu unless you hold a key. &lt;code&gt;menu-force&lt;/code&gt; = always show, no timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Glob for default entry, or &lt;code&gt;@saved&lt;/code&gt; to remember last choice in an EFI variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;editor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allow editing kernel cmdline at the menu. &lt;strong&gt;Disable on untrusted physical access&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console-mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;keep&lt;/code&gt; (default), &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, or a numeric mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;random-seed-mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;off&lt;/code&gt;, &lt;code&gt;with-system-token&lt;/code&gt; (default), or &lt;code&gt;always&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto-entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show/hide auto-discovered foreign loaders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto-firmware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show “Reboot into firmware” entry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also change timeout/default at runtime without editing 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="c"&gt;# Persistent default (EFI variable)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl set-default &lt;span class="s1"&gt;'&amp;lt;entry-id-or-glob&amp;gt;'&lt;/span&gt;

&lt;span class="c"&gt;# Next boot only&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl set-oneshot &lt;span class="s1"&gt;'&amp;lt;entry-id-or-glob&amp;gt;'&lt;/span&gt;

&lt;span class="c"&gt;# Menu timeout&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl set-timeout 5
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl set-timeout-oneshot 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Special IDs resolved by &lt;code&gt;bootctl&lt;/code&gt;: &lt;code&gt;@default&lt;/code&gt;, &lt;code&gt;@oneshot&lt;/code&gt;, &lt;code&gt;@current&lt;/code&gt;, &lt;code&gt;@saved&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From a running system you can also ask systemd to reboot into a specific entry or force the menu once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl reboot &lt;span class="nt"&gt;--boot-loader-entry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;entry-id&amp;gt;'&lt;/span&gt;
systemctl reboot &lt;span class="nt"&gt;--boot-loader-menu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
systemctl reboot &lt;span class="nt"&gt;--firmware-setup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those depend on the Boot Loader Interface that systemd-boot implements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type #1 entries you can read and reason about
&lt;/h2&gt;

&lt;p&gt;A minimal BLS Type #1 snippet looks like this (paths relative to the filesystem that holds the snippet):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# $BOOT/loader/entries/6a9857a393724b7a981ebb5b8495b9ea-6.12.0-amd64.conf
&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;      &lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="n"&gt;GNU&lt;/span&gt;/&lt;span class="n"&gt;Linux&lt;/span&gt;
&lt;span class="n"&gt;sort&lt;/span&gt;-&lt;span class="n"&gt;key&lt;/span&gt;   &lt;span class="n"&gt;debian&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt;-&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="n"&gt;a9857a393724b7a981ebb5b8495b9ea&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt;    &lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;amd64&lt;/span&gt;
&lt;span class="n"&gt;architecture&lt;/span&gt; &lt;span class="n"&gt;x64&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;    &lt;span class="n"&gt;root&lt;/span&gt;=&lt;span class="n"&gt;UUID&lt;/span&gt;=&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="n"&gt;d3376e4&lt;/span&gt;-&lt;span class="n"&gt;fc93&lt;/span&gt;-&lt;span class="m"&gt;4509&lt;/span&gt;-&lt;span class="m"&gt;95&lt;/span&gt;&lt;span class="n"&gt;ec&lt;/span&gt;-&lt;span class="n"&gt;a21d68011da2&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt;
&lt;span class="n"&gt;linux&lt;/span&gt;      /&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="n"&gt;a9857a393724b7a981ebb5b8495b9ea&lt;/span&gt;/&lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;amd64&lt;/span&gt;/&lt;span class="n"&gt;linux&lt;/span&gt;
&lt;span class="n"&gt;initrd&lt;/span&gt;     /&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="n"&gt;a9857a393724b7a981ebb5b8495b9ea&lt;/span&gt;/&lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;amd64&lt;/span&gt;/&lt;span class="n"&gt;initrd&lt;/span&gt;.&lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filenames are identifiers, not UI titles (restricted charset: alnum, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Prefer including &lt;strong&gt;entry-token/machine-id + kernel version&lt;/strong&gt; in the filename to avoid multi-boot clashes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; comes from &lt;code&gt;PRETTY_NAME&lt;/code&gt; when &lt;code&gt;kernel-install&lt;/code&gt; generates the file&lt;/li&gt;
&lt;li&gt;Multiple &lt;code&gt;initrd&lt;/code&gt; / &lt;code&gt;options&lt;/code&gt; lines are allowed and are combined in order&lt;/li&gt;
&lt;li&gt;On EFI, Linux images should be EFI PE stubs (&lt;code&gt;CONFIG_EFI_STUB&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You rarely hand-write these on a package-managed host. You let &lt;code&gt;kernel-install&lt;/code&gt; do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let kernel-install own kernel placement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See what the host will do for the running kernel&lt;/span&gt;
kernel-install inspect

&lt;span class="c"&gt;# Install current or explicit kernel&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;kernel-install add &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /boot/vmlinuz-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; /boot/initrd.img-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Or every kernel under /usr/lib/modules (where layout supports it)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;kernel-install add-all

&lt;span class="c"&gt;# Remove an old version cleanly&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;kernel-install remove 6.11.0-amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layout selection (&lt;code&gt;install.conf&lt;/code&gt; / env):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;layout=bls&lt;/code&gt; — Type #1 under &lt;code&gt;$BOOT/loader/entries/&lt;/code&gt; + files under &lt;code&gt;$BOOT/&amp;lt;entry-token&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layout=uki&lt;/code&gt; — Type #2 copy into &lt;code&gt;$BOOT/EFI/Linux/&amp;lt;entry-token&amp;gt;-&amp;lt;version&amp;gt;.efi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layout=auto&lt;/code&gt; — UKI if the image is a UKI; else BLS if entries already look BLS-like&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entry token selection (&lt;code&gt;bootctl install --entry-token=&lt;/code&gt; / &lt;code&gt;kernel-install --entry-token=&lt;/code&gt; / &lt;code&gt;/etc/kernel/entry-token&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;machine-id&lt;/code&gt; — best for multiple parallel installs of the same OS on one disk&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os-id&lt;/code&gt; / &lt;code&gt;os-image-id&lt;/code&gt; — stable names, but collide if two installs share the same ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auto&lt;/code&gt; — read &lt;code&gt;/etc/kernel/entry-token&lt;/code&gt; if present, else fall through machine-id → IMAGE_ID → ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inspect discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kernel-install list
bootctl list
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bootctl &lt;span class="nt"&gt;-x&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/loader/entries"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type #2 UKIs (when you want one signed PE file)
&lt;/h2&gt;

&lt;p&gt;Unified Kernel Images live as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$BOOT/EFI/Linux/&amp;lt;name&amp;gt;.efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bootctl&lt;/code&gt; can classify them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bootctl kernel-identify /path/to/vmlinuz.efi
bootctl kernel-inspect  /path/to/vmlinuz.efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible identify results: &lt;code&gt;uki&lt;/code&gt;, &lt;code&gt;addon&lt;/code&gt;, &lt;code&gt;pe&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Building UKIs is usually &lt;code&gt;ukify&lt;/code&gt; + (optionally) &lt;code&gt;systemd-measure&lt;/code&gt; for PCR policies—that is a full article on its own. For day-2 ops with systemd-boot you mainly need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UKIs land in &lt;code&gt;EFI/Linux/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bootctl list&lt;/code&gt; sees them&lt;/li&gt;
&lt;li&gt;Secure Boot signatures match your enrolled keys (again: &lt;code&gt;sbctl&lt;/code&gt; territory)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Early boot entropy: random-seed
&lt;/h2&gt;

&lt;p&gt;systemd-boot can pass a solid random seed into the OS very early by combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/loader/random-seed&lt;/code&gt; on the ESP&lt;/li&gt;
&lt;li&gt;a persistent &lt;code&gt;LoaderSystemToken&lt;/code&gt; EFI variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initialize both:&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;bootctl random-seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default &lt;code&gt;random-seed-mode with-system-token&lt;/code&gt; only credits the ESP seed when the system token exists—this avoids identical seeds when the same disk image is cloned to many machines without unique tokens.&lt;/p&gt;

&lt;p&gt;If the random-seed file is marked &lt;strong&gt;immutable&lt;/strong&gt; (read-only attribute), systemd-boot neither updates nor uses it—by design, because a never-updated seed would be identical every boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boot counting: automatic fallback after a bad kernel
&lt;/h2&gt;

&lt;p&gt;systemd-boot supports &lt;strong&gt;boot counting&lt;/strong&gt;: entry filenames encode tries-left / tries-done. On failure to reach a “good” boot, the loader prefers older entries.&lt;/p&gt;

&lt;p&gt;Userspace side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-bless-boot-generator&lt;/code&gt; detects counting is active&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-bless-boot.service&lt;/code&gt; runs once the boot is considered successful (&lt;code&gt;boot-complete.target&lt;/code&gt; path)&lt;/li&gt;
&lt;li&gt;It renames the entry to drop the counters (“bless” = permanently good)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/lib/systemd/systemd-bless-boot status
&lt;span class="c"&gt;# good | bad | indeterminate | clean | dirty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can force:&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; /usr/lib/systemd/systemd-bless-boot good
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/lib/systemd/systemd-bless-boot bad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;bad&lt;/code&gt; when you know the current entry is toxic and you want it skipped next time without burning remaining tries.&lt;/p&gt;

&lt;p&gt;This is the practical answer to “new kernel panic-loops and I have no serial console on site.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Housekeeping: unlink and cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove one entry and unreferenced payloads&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl &lt;span class="nb"&gt;unlink&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;entry-id-or-glob&amp;gt;'&lt;/span&gt;

&lt;span class="c"&gt;# Dry run first&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nb"&gt;unlink&lt;/span&gt; &lt;span class="s1"&gt;'*-6.11.*'&lt;/span&gt;

&lt;span class="c"&gt;# Remove orphaned files for this entry token that no entry references&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl cleanup
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl &lt;span class="nt"&gt;--dry-run&lt;/span&gt; cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;code&gt;kernel-install remove&lt;/code&gt; for package-shaped kernels so plugins (depmod, loaderentry, uki-copy) stay consistent. Use &lt;code&gt;bootctl unlink/cleanup&lt;/code&gt; for orphans and one-off images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-boot and foreign OS entries
&lt;/h2&gt;

&lt;p&gt;Out of the box, systemd-boot can surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft Windows Boot Manager&lt;/li&gt;
&lt;li&gt;macOS boot manager&lt;/li&gt;
&lt;li&gt;EFI shell&lt;/li&gt;
&lt;li&gt;Reboot into firmware setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toggle discovery with &lt;code&gt;auto-entries&lt;/code&gt; / &lt;code&gt;auto-firmware&lt;/code&gt; in &lt;code&gt;loader.conf&lt;/code&gt;. For “reboot into Windows tonight”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bootctl list   &lt;span class="c"&gt;# find the Windows entry id&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;bootctl set-oneshot auto-windows
&lt;span class="c"&gt;# or the concrete id shown by list&lt;/span&gt;
systemctl reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A sane baseline checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ESP mounted&lt;/strong&gt; where &lt;code&gt;bootctl --print-esp-path&lt;/code&gt; expects it; XBOOTLDR at &lt;code&gt;/boot&lt;/code&gt; if you use one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bootctl install&lt;/code&gt;&lt;/strong&gt; once; &lt;strong&gt;&lt;code&gt;bootctl update&lt;/code&gt;&lt;/strong&gt; after systemd-boot package upgrades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loader.conf&lt;/code&gt;&lt;/strong&gt;: short timeout, &lt;code&gt;editor no&lt;/code&gt; on exposed machines, &lt;code&gt;default&lt;/code&gt; glob or &lt;code&gt;@saved&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kernel-install&lt;/code&gt; layout&lt;/strong&gt; matches how you ship kernels (&lt;code&gt;bls&lt;/code&gt; vs &lt;code&gt;uki&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bootctl random-seed&lt;/code&gt;&lt;/strong&gt; on first provisioning of bare metal (and unique per machine).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boot counting + bless-boot&lt;/strong&gt; enabled if you want automatic bad-kernel fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; after every kernel install:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bootctl list
bootctl status
kernel-install inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secure Boot&lt;/strong&gt;: sign &lt;code&gt;systemd-boot&lt;/code&gt; and kernels/UKIs with &lt;em&gt;your&lt;/em&gt; enrolled keys—do not confuse “loader installed” with “firmware will execute it.”&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Rollback / coexistence notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Switching &lt;em&gt;to&lt;/em&gt; systemd-boot does not require wiping GRUB in the same second. Firmware boot order decides who runs. Keep a known-good GRUB entry until &lt;code&gt;bootctl list&lt;/code&gt; and a test reboot look right.&lt;/li&gt;
&lt;li&gt;Switching &lt;em&gt;away&lt;/em&gt;: &lt;code&gt;bootctl remove&lt;/code&gt; clears systemd-boot copies and firmware entries it owns; restore your previous loader deliberately.&lt;/li&gt;
&lt;li&gt;Soft-reboot and sysupdate operate &lt;em&gt;after&lt;/em&gt; or &lt;em&gt;around&lt;/em&gt; the bootloader; they do not replace it.&lt;/li&gt;
&lt;li&gt;Portable services / sysext extend the running OS; they are not boot entries.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/unstable/systemd-boot/systemd-boot.7.en.html" rel="noopener noreferrer"&gt;systemd-boot(7)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/unstable/systemd-boot/bootctl.1.en.html" rel="noopener noreferrer"&gt;bootctl(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/unstable/systemd/loader.conf.5.en.html" rel="noopener noreferrer"&gt;loader.conf(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/unstable/systemd/kernel-install.8.en.html" rel="noopener noreferrer"&gt;kernel-install(8)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/unstable/systemd-boot/systemd-bless-boot.service.8.en.html" rel="noopener noreferrer"&gt;systemd-bless-boot.service(8)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://uapi-group.org/specifications/specs/boot_loader_specification/" rel="noopener noreferrer"&gt;UAPI.1 Boot Loader Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Related: &lt;a href="https://manpages.debian.org/unstable/systemd-boot/systemd-stub.7.en.html" rel="noopener noreferrer"&gt;systemd-stub(7)&lt;/a&gt;, &lt;a href="https://manpages.debian.org/unstable/systemd-ukify/ukify.1.en.html" rel="noopener noreferrer"&gt;ukify(1)&lt;/a&gt;, &lt;a href="https://manpages.debian.org/unstable/systemd/systemd-measure.1.en.html" rel="noopener noreferrer"&gt;systemd-measure(1)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;GRUB still has a place on legacy BIOS and some exotic setups. On ordinary UEFI servers and workstations, systemd-boot + Boot Loader Spec entries + &lt;code&gt;bootctl&lt;/code&gt; is the smaller, auditable path: plain-text or UKI entries on a known partition, defaults in EFI variables, and a userspace tool that tells you the truth before you reboot.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Leaning on setuid sudo Alone: Practical run0 Privilege Elevation on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 18 Sep 2026 05:03:41 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-leaning-on-setuid-sudo-alone-practical-run0-privilege-elevation-on-linux-2jo8</link>
      <guid>https://dev.to/lyraalishaikh/stop-leaning-on-setuid-sudo-alone-practical-run0-privilege-elevation-on-linux-2jo8</guid>
      <description>&lt;h1&gt;
  
  
  Stop Leaning on setuid sudo Alone: Practical run0 Privilege Elevation on Linux
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; still works. It also still depends on a classic pattern: a &lt;strong&gt;setuid&lt;/strong&gt; helper, caller environment inheritance (unless tightly filtered), and a policy language that lives outside the service manager.&lt;/p&gt;

&lt;p&gt;systemd &lt;strong&gt;256+&lt;/strong&gt; ships a different elevation tool: &lt;strong&gt;&lt;code&gt;run0&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;run0&lt;/code&gt; is not a rebranded &lt;code&gt;sudoers&lt;/code&gt; parser. It is a multi-call alias of &lt;code&gt;systemd-run&lt;/code&gt; that starts your command as a &lt;strong&gt;fresh transient service&lt;/strong&gt;, authenticates through &lt;strong&gt;polkit&lt;/strong&gt;, allocates an &lt;strong&gt;independent pseudo-TTY&lt;/strong&gt; when you are on a terminal, and never relies on setuid/setgid bits to gain power. That makes it especially interesting on hardened hosts where &lt;code&gt;NoNewPrivileges=&lt;/code&gt; is on the table, and on fleets that already treat systemd as the source of truth for process isolation.&lt;/p&gt;

&lt;p&gt;This guide is written against the &lt;code&gt;run0(1)&lt;/code&gt; / &lt;code&gt;systemd-run(1)&lt;/code&gt; manuals and a live &lt;strong&gt;systemd 257&lt;/strong&gt; install (Debian trixie). Newer releases add more knobs; stick to the man page on your box when in doubt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem it solves
&lt;/h2&gt;

&lt;p&gt;Operator elevation usually fails in one of these ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The elevated process still carries too much of the caller’s environment and session assumptions.&lt;/li&gt;
&lt;li&gt;Authentication and the privileged program share the same TTY in ways that are hard to reason about.&lt;/li&gt;
&lt;li&gt;setuid helpers stop working (or become undesirable) when the system manager enforces &lt;code&gt;NoNewPrivileges=yes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One-off root work is invisible to cgroup accounting, journal unit filters, and slice placement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;run0&lt;/code&gt; attacks those points directly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No credential inheritance from the caller into the elevated command&lt;/strong&gt; — the service manager forks a clean service context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;polkit authentication&lt;/strong&gt;, with the prompt isolated from the terminal when possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent PTY&lt;/strong&gt; for the invoked command (default when stdio is a TTY).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SetUID/SetGID implementation path&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Sessions go through the &lt;strong&gt;&lt;code&gt;systemd-run0&lt;/code&gt; PAM stack&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already wrote &lt;code&gt;sudoers.d&lt;/code&gt; policy, keep it for command allowlists that polkit does not express well. If you already use plain &lt;code&gt;systemd-run&lt;/code&gt; to sandbox one-offs, keep that for non-interactive isolation. &lt;code&gt;run0&lt;/code&gt; sits in the middle: &lt;strong&gt;interactive (or scripted) privilege change that looks like sudo, but executes like a managed unit&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;systemd ≥ 256&lt;/strong&gt; for &lt;code&gt;run0&lt;/code&gt; itself (&lt;code&gt;--pty&lt;/code&gt; / &lt;code&gt;--pipe&lt;/code&gt; / shell prompt prefix land in &lt;strong&gt;257&lt;/strong&gt; on the manuals used here).&lt;/li&gt;
&lt;li&gt;Working &lt;strong&gt;polkit&lt;/strong&gt; (&lt;code&gt;polkitd&lt;/code&gt;) for non-root callers.&lt;/li&gt;
&lt;li&gt;Permission to manage units — by default that is polkit action &lt;code&gt;org.freedesktop.systemd1.manage-units&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check what you have:&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="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; run0
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&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; run0&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
run0 &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical install you should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemd 257 (257.x)
/usr/bin/run0 -&amp;gt; systemd-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm polkit is alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-active polkit
pkaction &lt;span class="nt"&gt;--action-id&lt;/span&gt; org.freedesktop.systemd1.manage-units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Debian/Ubuntu-style defaults, that action is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;allow_any&lt;/code&gt; / &lt;code&gt;allow_inactive&lt;/code&gt;: &lt;strong&gt;auth_admin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow_active&lt;/code&gt; (active local session): &lt;strong&gt;auth_admin_keep&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the stock admin identity rule often nominates &lt;strong&gt;&lt;code&gt;unix-group:sudo&lt;/code&gt;&lt;/strong&gt; as the administrative group. That does &lt;strong&gt;not&lt;/strong&gt; mean &lt;code&gt;run0&lt;/code&gt; reads &lt;code&gt;/etc/sudoers&lt;/code&gt;. It means polkit and sudo frequently share the same human admin group name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;you (uid=1000)
   │  run0 [options] command...
   ▼
polkit: org.freedesktop.systemd1.manage-units
   │  auth_admin / auth_admin_keep
   ▼
PID 1 starts a transient .service (often under user.slice for run0)
   │  fresh execution context
   │  optional independent PTY
   │  PAM stack: systemd-run0
   ▼
command runs as root (default) or --user=/--group=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important implementation detail from the manual: &lt;strong&gt;&lt;code&gt;run0&lt;/code&gt; is a symbolic link to &lt;code&gt;systemd-run&lt;/code&gt;&lt;/strong&gt;. Invoking the binary as &lt;code&gt;run0&lt;/code&gt; selects the elevation personality; invoking it as &lt;code&gt;systemd-run&lt;/code&gt; selects the general transient-unit tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Everyday elevation
&lt;/h2&gt;

&lt;p&gt;Interactive root shell (no command → shell):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;One-shot root command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nb"&gt;id
&lt;/span&gt;run0 &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/shadow | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;   &lt;span class="c"&gt;# careful: still privileged output&lt;/span&gt;
run0 systemctl restart chrony
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a specific shell explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/bin/bash
&lt;span class="c"&gt;# or&lt;/span&gt;
run0 /bin/bash &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that surprise sudo veterans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With no command, the shell defaults to the &lt;strong&gt;originating user’s shell&lt;/strong&gt;, not the target user’s shell (local case).&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;--machine=&lt;/code&gt;, the fallback shell is &lt;code&gt;/bin/sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Unlike &lt;code&gt;sudo -i&lt;/code&gt; folklore, &lt;code&gt;run0&lt;/code&gt; always uses &lt;strong&gt;login-shell semantics&lt;/strong&gt; for shells it starts (manual wording on current docs: always login shell semantics regardless of &lt;code&gt;-i&lt;/code&gt;-style habits from sudo).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2) Become another user, not only root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# service account shell / command&lt;/span&gt;
run0 &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www-data &lt;span class="nt"&gt;--group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www-data &lt;span class="nb"&gt;id
&lt;/span&gt;run0 &lt;span class="nt"&gt;-u&lt;/span&gt; postgres &lt;span class="nt"&gt;-g&lt;/span&gt; postgres &lt;span class="nt"&gt;--chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/postgresql psql &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# interactive as that user&lt;/span&gt;
run0 &lt;span class="nt"&gt;-u&lt;/span&gt; deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working directory rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root elevation: default cwd is the &lt;strong&gt;client’s current directory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Non-root target user: default cwd is that user’s &lt;strong&gt;home directory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Override with &lt;code&gt;-D&lt;/code&gt; / &lt;code&gt;--chdir=&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;-D&lt;/span&gt; /etc/nginx nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
run0 &lt;span class="nt"&gt;-u&lt;/span&gt; backup &lt;span class="nt"&gt;-D&lt;/span&gt; /var/backups /usr/local/bin/backup-now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3) See the unit, not just the exit code
&lt;/h2&gt;

&lt;p&gt;Because elevation is a transient service, you can observe it like any other 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="c"&gt;# In one terminal, start a longer session:&lt;/span&gt;
run0 &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin-shell.service

&lt;span class="c"&gt;# Elsewhere:&lt;/span&gt;
systemctl status admin-shell.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
systemctl show admin-shell.service &lt;span class="nt"&gt;-p&lt;/span&gt; MainPID &lt;span class="nt"&gt;-p&lt;/span&gt; User &lt;span class="nt"&gt;-p&lt;/span&gt; Slice &lt;span class="nt"&gt;-p&lt;/span&gt; ControlGroup
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; admin-shell.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;Useful properties to pass through &lt;code&gt;--property=&lt;/code&gt; (same assignment syntax as &lt;code&gt;systemctl set-property&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;# Hard ceiling example for a risky maintenance command&lt;/span&gt;
run0 &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512M &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TasksMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"compress logs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; /var/log &lt;span class="nt"&gt;-czf&lt;/span&gt; /root/logs.tgz &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# CPU niceness without a full policy rewrite&lt;/span&gt;
run0 &lt;span class="nt"&gt;--nice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 updatedb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place the session in a slice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;--slice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system-admin.slice bash
&lt;span class="c"&gt;# or inherit/nest relative to the caller’s slice&lt;/span&gt;
run0 &lt;span class="nt"&gt;--slice-inherit&lt;/span&gt; &lt;span class="nt"&gt;--slice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;maint bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default slice for &lt;code&gt;run0&lt;/code&gt; is &lt;strong&gt;&lt;code&gt;user.slice&lt;/code&gt;&lt;/strong&gt; (per &lt;code&gt;run0(1)&lt;/code&gt;), which is worth remembering if you expected &lt;code&gt;system.slice&lt;/code&gt; from bare &lt;code&gt;systemd-run --system&lt;/code&gt; habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  4) TTY vs pipe mode
&lt;/h2&gt;

&lt;p&gt;From systemd &lt;strong&gt;257&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pseudo-TTY&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--pty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interactive programs, shells, anything that wants a terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass-through stdio&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--pipe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pipelines and scripts where FDs should flow through&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;(default)&lt;/td&gt;
&lt;td&gt;If stdin/stdout/stderr are all TTYs → PTY; otherwise → pipe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Force pipeline-friendly behavior&lt;/span&gt;
run0 &lt;span class="nt"&gt;--pipe&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'gzip -c &amp;lt;/var/log/syslog'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/syslog.gz

&lt;span class="c"&gt;# Force a PTY even if you are unsure about detection&lt;/span&gt;
run0 &lt;span class="nt"&gt;--pty&lt;/span&gt; htop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security note from the manual: &lt;strong&gt;TTY isolation is a core feature&lt;/strong&gt;. Do not casually hand raw terminal control to untrusted programs when overriding defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  5) Environment: what you get and what you set
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;run0&lt;/code&gt; inherits the &lt;strong&gt;system manager environment&lt;/strong&gt;, not your full interactive profile dump. On top of that it sets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$TERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copied from caller (override with &lt;code&gt;--setenv&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$SUDO_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Originating username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$SUDO_UID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Originating UID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$SUDO_GID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Originating primary GID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$SHELL_PROMPT_PREFIX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default superhero emoji prefix when supported (257+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pass more with repeated &lt;code&gt;--setenv&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;run0 &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;EDITOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vim &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C.UTF-8 visudo &lt;span class="nt"&gt;-c&lt;/span&gt;
run0 &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HTTP_PROXY   &lt;span class="c"&gt;# value taken from caller env when =value omitted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prompt cosmetics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;--shell-prompt-prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'(root) '&lt;/span&gt;
&lt;span class="c"&gt;# or disable&lt;/span&gt;
run0 &lt;span class="nt"&gt;--shell-prompt-prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; bash
&lt;span class="c"&gt;# or via env for defaults&lt;/span&gt;
&lt;span class="nv"&gt;SYSTEMD_RUN_SHELL_PROMPT_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; run0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background tint (visual “you are elevated” cue):&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;# default: reddish as root, yellowish as other UID&lt;/span&gt;
run0 &lt;span class="nt"&gt;--background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;44 bash    &lt;span class="c"&gt;# blue&lt;/span&gt;
run0 &lt;span class="nt"&gt;--background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; bash      &lt;span class="c"&gt;# disable tint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6) polkit: where policy actually lives
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;run0&lt;/code&gt; does not consult &lt;code&gt;sudoers&lt;/code&gt; for the elevation decision. The gate is polkit’s unit-management action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkaction &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nt"&gt;--action-id&lt;/span&gt; org.freedesktop.systemd1.manage-units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect a description like “Manage system services or other units” and defaults requiring administrative authentication.&lt;/p&gt;

&lt;p&gt;Debian’s stock rule file commonly includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /usr/share/polkit-1/rules.d/50-default.rules (vendor file — do not edit in place)&lt;/span&gt;
&lt;span class="nx"&gt;polkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAdminRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unix-group:sudo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local overrides belong under &lt;code&gt;/etc/polkit-1/rules.d/&lt;/code&gt; (or the distribution’s documented local path). Example &lt;strong&gt;lab-only&lt;/strong&gt; pattern for a dedicated admin group active on the local console — tighten this for production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /etc/polkit-1/rules.d/60-admin-manage-units.rules&lt;/span&gt;
&lt;span class="c1"&gt;// Example only: prefer narrow groups + active-session checks in real fleets.&lt;/span&gt;
&lt;span class="nx"&gt;polkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org.freedesktop.systemd1.manage-units&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;polkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;YES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After changing rules, reload/restart polkit according to your distro (&lt;code&gt;systemctl restart polkit&lt;/code&gt; is the blunt approach; some setups pick up rules automatically).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--no-ask-password&lt;/code&gt; skips interactive auth prompts. That is appropriate for already-authorized automation contexts, not as a way to bypass policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  7) Why this pairs with NoNewPrivileges=
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;systemd-system.conf(5)&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;# /etc/systemd/system.conf.d/10-nnp.conf  # powerful; understand the blast radius
&lt;/span&gt;&lt;span class="nn"&gt;[Manager]&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;When true, PID 1 and its children cannot gain new privileges through &lt;code&gt;execve(2)&lt;/code&gt; via setuid/setgid bits or file capabilities. Classic &lt;code&gt;sudo&lt;/code&gt; is exactly that class of helper. &lt;code&gt;run0&lt;/code&gt; is documented as the elevation approach that still works in environments where setuid support is unavailable, because &lt;strong&gt;the service manager starts the privileged unit&lt;/strong&gt; instead of a setuid binary flipping credentials in-process.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; flip &lt;code&gt;NoNewPrivileges=&lt;/code&gt; globally on a general-purpose distro without a test plan — lots of legacy tooling still assumes setuid. Use it where you intentionally build a setuid-free image.&lt;/p&gt;

&lt;h2&gt;
  
  
  8) Containers and machines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Elevate inside a local machine/container known to machined&lt;/span&gt;
run0 &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;webtest systemctl status nginx &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
run0 &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;webtest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same &lt;code&gt;--machine=&lt;/code&gt; plumbing family as &lt;code&gt;systemd-run&lt;/code&gt; / &lt;code&gt;machinectl&lt;/code&gt;, not SSH.&lt;/p&gt;

&lt;h2&gt;
  
  
  9) Practical operator cookbook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Drop into root for a change window
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;change-window.service &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2026-09-18 change window"&lt;/span&gt;
&lt;span class="c"&gt;# work...&lt;/span&gt;
&lt;span class="c"&gt;# exit&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; change-window.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"10 min ago"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Restart a unit without keeping a root shell
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 systemctl try-restart caddy.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Edit a root-owned file with your $EDITOR semantics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EDITOR &lt;span class="nt"&gt;--setenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERM &lt;span class="nt"&gt;--chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/ssh &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'"$EDITOR" sshd_config'&lt;/span&gt;
sshd &lt;span class="nt"&gt;-t&lt;/span&gt;   &lt;span class="c"&gt;# still verify as root if needed: run0 sshd -t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bounded maintenance job
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apt-maintenance.service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;Nice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryHigh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TasksMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"apt update &amp;amp;&amp;amp; upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-lc&lt;/span&gt; &lt;span class="s1"&gt;'apt-get update &amp;amp;&amp;amp; apt-get -y upgrade'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify identity and origin fields inside the session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run0 bash &lt;span class="nt"&gt;-lc&lt;/span&gt; &lt;span class="s1"&gt;'id; printf "SUDO_USER=%s SUDO_UID=%s SUDO_GID=%s\n" \
  "$SUDO_USER" "$SUDO_UID" "$SUDO_GID"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see root (or the target user) for &lt;code&gt;id&lt;/code&gt;, and your original account in the &lt;code&gt;SUDO_*&lt;/code&gt; fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  10) Failure modes and debugging
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Immediate auth failure&lt;/td&gt;
&lt;td&gt;polkit denied / no admin group&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pkaction&lt;/code&gt;, group membership, active session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on console, fails over SSH&lt;/td&gt;
&lt;td&gt;agent / session class differences&lt;/td&gt;
&lt;td&gt;run on a real active session; install a polkit agent if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command not found inside run0&lt;/td&gt;
&lt;td&gt;clean service &lt;code&gt;PATH&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;use absolute paths or &lt;code&gt;--setenv=PATH=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive TUI garbled&lt;/td&gt;
&lt;td&gt;pipe mode instead of PTY&lt;/td&gt;
&lt;td&gt;pass &lt;code&gt;--pty&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit left failed&lt;/td&gt;
&lt;td&gt;command non-zero exit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl reset-failed&lt;/code&gt; / &lt;code&gt;journalctl -u …&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Looks like sudo but ignore sudoers”&lt;/td&gt;
&lt;td&gt;expected&lt;/td&gt;
&lt;td&gt;policy is polkit + unit properties, not &lt;code&gt;sudoers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Debug trail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; polkit.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-t&lt;/span&gt; run0 &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;  &lt;span class="c"&gt;# if tagged on your build&lt;/span&gt;
systemctl list-units &lt;span class="s1"&gt;'run-*.service'&lt;/span&gt; &lt;span class="nt"&gt;--all&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;
  
  
  11) What run0 is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a full &lt;code&gt;sudoers&lt;/code&gt; replacement&lt;/strong&gt; for per-command argument filtering, &lt;code&gt;NOPASSWD&lt;/code&gt; host-by-host matrices, or existing enterprise sudo policy distributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; the same article as “sandbox any one-off with &lt;code&gt;systemd-run&lt;/code&gt;” — that tool is broader (timers, scopes, user manager, etc.) and is not specifically the elevation UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;pkexec&lt;/code&gt; with a different name — both use polkit, but &lt;code&gt;run0&lt;/code&gt; is explicitly a systemd transient-service elevation path with unit properties, slices, and journal integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a substitute for service hardening (&lt;code&gt;ProtectSystem=&lt;/code&gt;, &lt;code&gt;CapabilityBoundingSet=&lt;/code&gt;, Landlock, seccomp). Elevation gets you in; hardening still belongs on long-running units.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migration cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old habit&lt;/th&gt;
&lt;th&gt;run0-shaped habit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo -s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo -u postgres -i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run0 -u postgres&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo -E cmd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;selective &lt;code&gt;run0 --setenv=NAME&lt;/code&gt; (prefer explicit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo nice -n 10 cmd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run0 --nice=10 cmd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo systemd-run …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;often just &lt;code&gt;run0 --property=…&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;audit via sudo logs only&lt;/td&gt;
&lt;td&gt;also &lt;code&gt;journalctl -u &amp;lt;unit&amp;gt;&lt;/code&gt; + polkit events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep &lt;code&gt;sudo&lt;/code&gt; installed while you migrate muscle memory. Many images will ship both for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout suggestion
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm systemd ≥ 256 and polkit healthy.&lt;/li&gt;
&lt;li&gt;Practice &lt;code&gt;run0 id&lt;/code&gt; and &lt;code&gt;run0&lt;/code&gt; shells on a lab user in the admin group.&lt;/li&gt;
&lt;li&gt;Teach absolute paths and &lt;code&gt;--setenv&lt;/code&gt; instead of blanket env preservation.&lt;/li&gt;
&lt;li&gt;Move break-glass interactive work to &lt;code&gt;run0 --unit=…&lt;/code&gt; so sessions are nameable in the journal.&lt;/li&gt;
&lt;li&gt;Only then consider narrowing sudoers or building setuid-free images that rely on service-manager elevation.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run0(1)&lt;/code&gt; — privilege elevation; multi-call alias of &lt;code&gt;systemd-run&lt;/code&gt; (systemd 256+)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-run(1)&lt;/code&gt; — transient services/scopes and shared options&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-system.conf(5)&lt;/code&gt; — &lt;code&gt;NoNewPrivileges=&lt;/code&gt; manager setting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;polkit(8)&lt;/code&gt; / &lt;code&gt;pkaction(1)&lt;/code&gt; — authorization actions and admin rules&lt;/li&gt;
&lt;li&gt;Debian policy file &lt;code&gt;org.freedesktop.systemd1.policy&lt;/code&gt; — &lt;code&gt;manage-units&lt;/code&gt; defaults (&lt;code&gt;auth_admin&lt;/code&gt; / &lt;code&gt;auth_admin_keep&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Vendor rules such as &lt;code&gt;/usr/share/polkit-1/rules.d/50-default.rules&lt;/code&gt; — admin group mapping (often &lt;code&gt;unix-group:sudo&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Local verification base for this article: systemd &lt;strong&gt;257.13&lt;/strong&gt; on Debian trixie, &lt;code&gt;run0 -&amp;gt; systemd-run&lt;/code&gt;, polkitd present&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; taught a generation of admins to borrow root carefully. &lt;code&gt;run0&lt;/code&gt; keeps the careful part, drops the setuid helper, and makes the elevated session a first-class systemd citizen. If your hosts already boot, isolate, and heal through units, elevating through units is the consistent next step.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Patching the Live Root: Practical A/B OS Updates with systemd-sysupdate</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 13 Sep 2026 05:03:44 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-patching-the-live-root-practical-ab-os-updates-with-systemd-sysupdate-g08</link>
      <guid>https://dev.to/lyraalishaikh/stop-patching-the-live-root-practical-ab-os-updates-with-systemd-sysupdate-g08</guid>
      <description>&lt;h1&gt;
  
  
  Stop Patching the Live Root: Practical A/B OS Updates with systemd-sysupdate
&lt;/h1&gt;

&lt;p&gt;Package managers are great at evolving a mutable system in place. They are a poorer fit when you want the opposite model: ship a &lt;strong&gt;whole, known-good OS image&lt;/strong&gt;, keep the currently running root untouched while the next version lands beside it, then flip roles on reboot.&lt;/p&gt;

&lt;p&gt;That is the job of &lt;strong&gt;&lt;code&gt;systemd-sysupdate&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is the image-update tool that pairs naturally with GPT discoverable partitions, dm-verity roots, UKIs on the ESP, &lt;code&gt;systemd-repart&lt;/code&gt; spare slots, and portable/container disk images. You describe &lt;em&gt;transfers&lt;/em&gt; (source → target). The tool enumerates versions, verifies downloads, writes into free A/B (or A/B/C…) slots, and only then finalizes names/labels so a reboot can pick up the new set.&lt;/p&gt;

&lt;p&gt;This article is a practical operator guide based on the current &lt;code&gt;systemd-sysupdate(8)&lt;/code&gt; and &lt;code&gt;sysupdate.d(5)&lt;/code&gt; manuals. The feature is still marked &lt;strong&gt;experimental&lt;/strong&gt; upstream—pin versions, read the man pages for your release, and expect setting names to evolve—but the core model has been stable enough since systemd &lt;strong&gt;251&lt;/strong&gt; to use deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem it solves
&lt;/h2&gt;

&lt;p&gt;Typical in-place upgrade pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mid-upgrade reboot leaves a half-applied userspace.&lt;/li&gt;
&lt;li&gt;Kernel, initrd, rootfs, and verity data can drift out of lockstep.&lt;/li&gt;
&lt;li&gt;“Rollback” means hoping the package manager’s undo path still works.&lt;/li&gt;
&lt;li&gt;Mutable &lt;code&gt;/&lt;/code&gt; makes fleet drift inevitable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;systemd-sysupdate&lt;/code&gt; flips the model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep &lt;strong&gt;multiple concurrent versions&lt;/strong&gt; of each resource (files, directories/subvolumes, or GPT partitions).&lt;/li&gt;
&lt;li&gt;Download the &lt;strong&gt;next&lt;/strong&gt; version into empty slots while the &lt;strong&gt;current&lt;/strong&gt; version keeps running.&lt;/li&gt;
&lt;li&gt;Bind several resources together with a shared &lt;strong&gt;version id&lt;/strong&gt; (&lt;code&gt;@v&lt;/code&gt;) so root + verity + UKI move as one logical release.&lt;/li&gt;
&lt;li&gt;Activate by booting the new entry point (usually the new UKI / boot loader entry), not by rewriting the live root underneath PID 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It can update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the host OS online from inside itself&lt;/li&gt;
&lt;li&gt;offline disk images via &lt;code&gt;--image=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;container images, portable service images, and other file/dir trees the same way&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mental model: transfers + versions
&lt;/h2&gt;

&lt;p&gt;Each resource you update is one &lt;strong&gt;transfer&lt;/strong&gt; file under:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/sysupdate.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/sysupdate.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/sysupdate.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/sysupdate.d/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Current docs name these &lt;code&gt;*.transfer&lt;/code&gt; (older trees and some man page editions still show &lt;code&gt;*.conf&lt;/code&gt;—check &lt;code&gt;man 5 sysupdate.d&lt;/code&gt; on your box).&lt;/p&gt;

&lt;p&gt;Every transfer has three sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[Transfer]&lt;/code&gt;&lt;/strong&gt; — policy (min version, protect running version, verify, optional features)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/strong&gt; — where versions come from&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[Target]&lt;/code&gt;&lt;/strong&gt; — where versions are installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources and targets are typed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source type&lt;/th&gt;
&lt;th&gt;Typical target&lt;/th&gt;
&lt;th&gt;Auth / integrity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url-file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;regular-file&lt;/code&gt; or &lt;code&gt;partition&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SHA256SUMS + optional SHA256SUMS.gpg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url-tar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;directory&lt;/code&gt; or &lt;code&gt;subvolume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regular-file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;regular-file&lt;/code&gt; or &lt;code&gt;partition&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no remote auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;directory&lt;/code&gt; / &lt;code&gt;subvolume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;local only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;directory&lt;/code&gt; / &lt;code&gt;subvolume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;directory&lt;/code&gt; / &lt;code&gt;subvolume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;local copy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HTTP(S) catalogs are deliberately simple: a GNU &lt;code&gt;sha256sum&lt;/code&gt;-format &lt;strong&gt;&lt;code&gt;SHA256SUMS&lt;/code&gt;&lt;/strong&gt; next to the payloads, optionally signed as &lt;strong&gt;&lt;code&gt;SHA256SUMS.gpg&lt;/code&gt;&lt;/strong&gt;. Payloads are always checked against the hashes. &lt;code&gt;Verify=&lt;/code&gt; (default &lt;strong&gt;yes&lt;/strong&gt;) controls whether the manifest signature is validated against the import keyring (&lt;code&gt;/usr/lib/systemd/import-pubring.pgp&lt;/code&gt; and &lt;code&gt;/etc/systemd/import-pubring.pgp&lt;/code&gt;, with older &lt;code&gt;.gpg&lt;/code&gt; names still documented).&lt;/p&gt;

&lt;p&gt;Version extraction uses &lt;strong&gt;match patterns&lt;/strong&gt;. The mandatory wildcard is &lt;strong&gt;&lt;code&gt;@v&lt;/code&gt;&lt;/strong&gt;. Useful extras include partition UUID/flags (&lt;code&gt;@u&lt;/code&gt;, &lt;code&gt;@f&lt;/code&gt;, &lt;code&gt;@a&lt;/code&gt;, &lt;code&gt;@g&lt;/code&gt;, &lt;code&gt;@r&lt;/code&gt;), file mode/size/time, boot-assessment counters (&lt;code&gt;@d&lt;/code&gt; / &lt;code&gt;@l&lt;/code&gt;), and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete OS update: root + verity + UKI
&lt;/h2&gt;

&lt;p&gt;The manuals’ canonical example is still the clearest. Updating “foobarOS” to version &lt;code&gt;47&lt;/code&gt; means three coordinated transfers of the same &lt;code&gt;@v&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;foobarOS_47.root.xz&lt;/code&gt; → empty GPT &lt;strong&gt;root&lt;/strong&gt; partition (x86-64 type UUID &lt;code&gt;4f68bce3-e8cd-4db1-96e7-fbcaf984b709&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;foobarOS_47.verity.xz&lt;/code&gt; → empty GPT &lt;strong&gt;root verity&lt;/strong&gt; partition (&lt;code&gt;2c7357ed-ebd2-46d9-aec1-23d437ec2bf5&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;foobarOS_47.efi&lt;/code&gt; → &lt;code&gt;$BOOT/EFI/Linux/foobarOS_47.efi&lt;/code&gt; (Boot Loader Spec Type #2 UKI)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Empty partition slots are marked with GPT label &lt;strong&gt;&lt;code&gt;_empty&lt;/code&gt;&lt;/strong&gt;. After a successful write, labels become versioned names like &lt;code&gt;foobarOS_47&lt;/code&gt; / &lt;code&gt;foobarOS_47_verity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical ordering rule:&lt;/strong&gt; transfers run in &lt;strong&gt;alphabetical filename order&lt;/strong&gt;. Download+write happens first for all transfers; final rename/relabel happens second, still in that order, with disk sync points. Put the &lt;strong&gt;boot entry point last&lt;/strong&gt; (for example &lt;code&gt;90-uki.transfer&lt;/code&gt;) so a crash never leaves a bootable UKI pointing at incomplete root/verity backing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example transfer set
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/sysupdate.d/50-root.transfer
&lt;/span&gt;&lt;span class="nn"&gt;[Transfer]&lt;/span&gt;
&lt;span class="py"&gt;ProtectVersion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%A&lt;/span&gt;
&lt;span class="py"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="nn"&gt;[Source]&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;url-file&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://download.example.com/foobarOS&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v.root.xz&lt;/span&gt;

&lt;span class="nn"&gt;[Target]&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;partition&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v&lt;/span&gt;
&lt;span class="c"&gt;# Prefer DPS type names when your systemd supports them; otherwise use the GPT UUID.
&lt;/span&gt;&lt;span class="py"&gt;TypeUUID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root-x86-64&lt;/span&gt;
&lt;span class="py"&gt;InstancesMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;ReadOnly&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/sysupdate.d/60-verity.transfer
&lt;/span&gt;&lt;span class="nn"&gt;[Transfer]&lt;/span&gt;
&lt;span class="py"&gt;ProtectVersion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%A&lt;/span&gt;
&lt;span class="py"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="nn"&gt;[Source]&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;url-file&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://download.example.com/foobarOS&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v.verity.xz&lt;/span&gt;

&lt;span class="nn"&gt;[Target]&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;partition&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v_verity&lt;/span&gt;
&lt;span class="py"&gt;TypeUUID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root-x86-64-verity&lt;/span&gt;
&lt;span class="py"&gt;InstancesMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;ReadOnly&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/sysupdate.d/90-uki.transfer
&lt;/span&gt;&lt;span class="nn"&gt;[Transfer]&lt;/span&gt;
&lt;span class="py"&gt;ProtectVersion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%A&lt;/span&gt;
&lt;span class="py"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="nn"&gt;[Source]&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;url-file&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://download.example.com/foobarOS&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v.efi&lt;/span&gt;

&lt;span class="nn"&gt;[Target]&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;regular-file&lt;/span&gt;
&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/EFI/Linux&lt;/span&gt;
&lt;span class="py"&gt;PathRelativeTo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;boot&lt;/span&gt;
&lt;span class="py"&gt;MatchPattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;foobarOS_@v.efi&lt;/span&gt;
&lt;span class="py"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0644&lt;/span&gt;
&lt;span class="py"&gt;InstancesMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that save real debugging time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ProtectVersion=%A&lt;/code&gt;&lt;/strong&gt; expands from &lt;code&gt;IMAGE_VERSION=&lt;/code&gt; in &lt;code&gt;/etc/os-release&lt;/code&gt; and refuses to delete/overwrite the currently booted image version while making room for the next one. &lt;code&gt;%w&lt;/code&gt; (&lt;code&gt;VERSION_ID=&lt;/code&gt;) and &lt;code&gt;%B&lt;/code&gt; (&lt;code&gt;BUILD_ID=&lt;/code&gt;) are related alternatives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Path=auto&lt;/code&gt;&lt;/strong&gt; on partition targets means “the block device that backs the booted root.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PathRelativeTo=boot&lt;/code&gt;&lt;/strong&gt; (also &lt;code&gt;esp&lt;/code&gt;, &lt;code&gt;xbootldr&lt;/code&gt;, &lt;code&gt;root&lt;/code&gt;, …) anchors file targets relative to the right firmware/boot volume instead of hard-coding mount points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;InstancesMax=&lt;/code&gt;&lt;/strong&gt; should usually match across the whole transfer set. With two root slots you effectively keep current + next.&lt;/li&gt;
&lt;li&gt;Partition targets &lt;strong&gt;must already exist&lt;/strong&gt;. &lt;code&gt;systemd-sysupdate&lt;/code&gt; will not create GPT entries. Pair this with &lt;strong&gt;&lt;code&gt;systemd-repart&lt;/code&gt;&lt;/strong&gt; so first boot (or image build) materializes enough &lt;code&gt;_empty&lt;/code&gt; slots of the right types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server-side layout
&lt;/h3&gt;

&lt;p&gt;On the download host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://download.example.com/foobarOS/SHA256SUMS
https://download.example.com/foobarOS/SHA256SUMS.gpg
https://download.example.com/foobarOS/foobarOS_47.root.xz
https://download.example.com/foobarOS/foobarOS_47.verity.xz
https://download.example.com/foobarOS/foobarOS_47.efi
https://download.example.com/foobarOS/foobarOS_48.root.xz
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the manifest with GNU &lt;code&gt;sha256sum&lt;/code&gt; (binary mode is recommended in the man page even on Linux):&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;cd&lt;/span&gt; /srv/foobarOS
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;--binary&lt;/span&gt; foobarOS_&lt;span class="k"&gt;*&lt;/span&gt;.root.xz foobarOS_&lt;span class="k"&gt;*&lt;/span&gt;.verity.xz foobarOS_&lt;span class="k"&gt;*&lt;/span&gt;.efi &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; SHA256SUMS
gpg &lt;span class="nt"&gt;--detach-sign&lt;/span&gt; &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; SHA256SUMS.gpg SHA256SUMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional freshness brake: include a special &lt;code&gt;BEST-BEFORE-YYYY-MM-DD&lt;/code&gt; entry in &lt;code&gt;SHA256SUMS&lt;/code&gt;. Past that date, the listing is rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day-2 commands operators actually use
&lt;/h2&gt;

&lt;p&gt;Install the tool first. On Debian/Ubuntu it typically ships in the &lt;strong&gt;&lt;code&gt;systemd-container&lt;/code&gt;&lt;/strong&gt; package; some other distros ship it with core systemd. 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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; systemd-sysupdate
man 8 systemd-sysupdate
man 5 sysupdate.d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enumerate and inspect
&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;# Default command is list&lt;/span&gt;
systemd-sysupdate list
systemd-sysupdate list 48          &lt;span class="c"&gt;# detail one version + required transfers&lt;/span&gt;
systemd-sysupdate &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See if anything newer exists
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nv"&gt;ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;systemd-sysupdate check-new&lt;span class="si"&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;"candidate: &lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"already current or no catalog"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit status &lt;strong&gt;0&lt;/strong&gt; means a newer installable version exists; its id is printed on stdout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install / stage
&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;# Newest available&lt;/span&gt;
systemd-sysupdate update

&lt;span class="c"&gt;# Explicit version&lt;/span&gt;
systemd-sysupdate update 48

&lt;span class="c"&gt;# Download now, install later (newer systemd)&lt;/span&gt;
systemd-sysupdate acquire 48
systemd-sysupdate update &lt;span class="nt"&gt;--offline&lt;/span&gt; 48

&lt;span class="c"&gt;# Install then reboot immediately (host OS only)&lt;/span&gt;
systemd-sysupdate update &lt;span class="nt"&gt;--reboot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;acquire&lt;/code&gt; (added in &lt;strong&gt;260&lt;/strong&gt; on current docs) separates bandwidth work from the finalization window. Older releases only have &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Space management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysupdate vacuum
systemd-sysupdate &lt;span class="nt"&gt;--instances-max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vacuum&lt;/code&gt; deletes or empties old instances until &lt;code&gt;InstancesMax=&lt;/code&gt; is satisfied. &lt;code&gt;update&lt;/code&gt;/&lt;code&gt;acquire&lt;/code&gt; already call this implicitly when they need a free slot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pending activation
&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;# Newer installed than running IMAGE_VERSION= ?&lt;/span&gt;
systemd-sysupdate pending &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reboot to activate"&lt;/span&gt;

&lt;span class="c"&gt;# Reboot only if pending&lt;/span&gt;
systemd-sysupdate reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pending&lt;/code&gt; compares the newest &lt;strong&gt;installed&lt;/strong&gt; version id against &lt;code&gt;IMAGE_VERSION=&lt;/code&gt; in &lt;code&gt;/etc/os-release&lt;/code&gt;. That is your “update written, not yet booted” signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Components (update planes that move independently)
&lt;/h3&gt;

&lt;p&gt;Resources that must always ship together belong in the &lt;strong&gt;same&lt;/strong&gt; &lt;code&gt;sysupdate.d/&lt;/code&gt; directory as multiple transfers.&lt;/p&gt;

&lt;p&gt;Resources that may move on different cadences get &lt;strong&gt;components&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysupdate components
systemd-sysupdate &lt;span class="nt"&gt;--component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;addon list
systemd-sysupdate &lt;span class="nt"&gt;--component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;addon update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That switches the search path to &lt;code&gt;/etc/sysupdate.&amp;lt;name&amp;gt;.d/&lt;/code&gt; (and the &lt;code&gt;/run&lt;/code&gt; + &lt;code&gt;/usr/lib&lt;/code&gt; twins). Do &lt;strong&gt;not&lt;/strong&gt; split root/verity/UKI across components if they must stay version-locked.&lt;/p&gt;

&lt;p&gt;Newer systemd also adds optional &lt;strong&gt;features&lt;/strong&gt;, &lt;code&gt;enable-feature&lt;/code&gt; / &lt;code&gt;disable-feature&lt;/code&gt;, &lt;code&gt;enable-component&lt;/code&gt; / &lt;code&gt;disable-component&lt;/code&gt;, and &lt;code&gt;cleanup&lt;/code&gt; for orphaned filesystem installs. Use them when your image product actually exposes optional payload sets; ignore them for a minimal A/B OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offline image maintenance
&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;# Apply transfers defined inside a disk image, writing partitions in-image&lt;/span&gt;
systemd-sysupdate &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/machines/edge.raw update

&lt;span class="c"&gt;# Or point at an explicit definitions directory&lt;/span&gt;
systemd-sysupdate &lt;span class="nt"&gt;--definitions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/transfers.d &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/appliance.img update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful for factory-updating nspawn machines, portable service images, or appliance RAW files from a build host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timers: download by day, reboot by night
&lt;/h2&gt;

&lt;p&gt;Do not couple “fetch” and “bounce the fleet” into one unit.&lt;/p&gt;

&lt;p&gt;Current unit names (check your release; older trees used &lt;code&gt;systemd-sysupdate.service&lt;/code&gt; / &lt;code&gt;.timer&lt;/code&gt; without the &lt;code&gt;-update&lt;/code&gt; infix):&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;# Pull updates on a schedule&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; systemd-sysupdate-update.timer

&lt;span class="c"&gt;# Optionally reboot later when a newer installed version is pending&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; systemd-sysupdate-reboot.timer

systemctl list-timers &lt;span class="s1"&gt;'systemd-sysupdate*'&lt;/span&gt;
systemctl status systemd-sysupdate-update.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why separate?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloads can be frequent and opportunistic.&lt;/li&gt;
&lt;li&gt;Reboots need change windows, draining, and monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pending&lt;/code&gt; + reboot timer gives you “already staged, activate overnight” without re-hitting the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some releases also ship &lt;code&gt;systemd-sysupdate-auto-enable.service&lt;/code&gt; to auto-enable suggested components/features before each update. Leave it off unless you explicitly want that product behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partition prep with systemd-repart
&lt;/h2&gt;

&lt;p&gt;Remember: sysupdate &lt;strong&gt;fills&lt;/strong&gt; slots; it does not invent them.&lt;/p&gt;

&lt;p&gt;A minimal repart mental model for two root + two verity slots:&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/repart.d/10-root-a.conf
&lt;/span&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;_empty&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;SizeMaxBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4G&lt;/span&gt;

&lt;span class="c"&gt;# /etc/repart.d/11-root-b.conf
&lt;/span&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;_empty&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;SizeMaxBytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4G&lt;/span&gt;

&lt;span class="c"&gt;# /etc/repart.d/20-verity-a.conf
&lt;/span&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-verity&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;_empty&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;128M&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;128M&lt;/span&gt;

&lt;span class="c"&gt;# /etc/repart.d/21-verity-b.conf
&lt;/span&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-verity&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;_empty&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;128M&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;128M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact &lt;code&gt;Type=&lt;/code&gt; aliases and verity pairing rules come from the UAPI Discoverable Partitions Specification and &lt;code&gt;systemd-repart(8)&lt;/code&gt;. Factory images often bake these empty slots at build time; devices can also grow them on first boot from free disk space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification checklist after first staging
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) Catalog vs installed&lt;/span&gt;
systemd-sysupdate list

&lt;span class="c"&gt;# 2) os-release identity the tool uses for pending/protect&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(IMAGE_ID|IMAGE_VERSION|VERSION_ID)='&lt;/span&gt; /etc/os-release

&lt;span class="c"&gt;# 3) Partition labels / empty slots&lt;/span&gt;
lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; NAME,PARTTYPENAME,PARTLABEL,SIZE,FSTYPE
&lt;span class="c"&gt;# or&lt;/span&gt;
sfdisk &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/disk/by-id/… | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'label:|type='&lt;/span&gt;

&lt;span class="c"&gt;# 4) UKIs present on $BOOT&lt;/span&gt;
bootctl status
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /efi/EFI/Linux 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /boot/EFI/Linux

&lt;span class="c"&gt;# 5) After update, before reboot&lt;/span&gt;
systemd-sysupdate pending&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo exit&lt;/span&gt;:&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;list&lt;/code&gt; sees remote versions but &lt;code&gt;update&lt;/code&gt; will not move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPG verify failing (&lt;code&gt;Verify=yes&lt;/code&gt; + missing key in import-pubring)&lt;/li&gt;
&lt;li&gt;no free &lt;code&gt;_empty&lt;/code&gt; / reclaimable slot (&lt;code&gt;InstancesMax&lt;/code&gt; / partition count)&lt;/li&gt;
&lt;li&gt;mismatched &lt;code&gt;@v&lt;/code&gt; sets (root 48 present remotely but verity 48 missing → incomplete version)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MinVersion=&lt;/code&gt; filtering candidates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ProtectVersion=&lt;/code&gt; preventing reclamation of the only spare that still holds the running image&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Boundaries: when not to use this
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;apt/dnf/zypper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mutable package OS, rich dependency solves, classic servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ostree / rpm-ostree / bootc&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Content-addressed tree deployments with their own client stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd-sysupdate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whole-file / whole-partition A/B transfers driven by simple HTTPS manifests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd-repart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create/grow GPT layout and factories—not the updater&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dm-verity / fs-verity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Integrity of what you already have—not distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;portable services / sysext&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add software onto a base OS—not replacing the base OS image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;soft-reboot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Userspace restart without firmware/kernel; complementary activation tactic, not an image transporter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are mostly pinning a few packages on Debian stable, stay with APT. If you are building appliances, edge nodes, kiosks, or verified image fleets where “version 48” means a concrete root+verity+UKI tuple, sysupdate is the systemd-native answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer rollout pattern
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Build images with &lt;code&gt;IMAGE_ID=&lt;/code&gt; / &lt;code&gt;IMAGE_VERSION=&lt;/code&gt; baked into &lt;code&gt;/etc/os-release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Publish payloads + &lt;code&gt;SHA256SUMS&lt;/code&gt; (+ &lt;code&gt;.gpg&lt;/code&gt;) on HTTPS.&lt;/li&gt;
&lt;li&gt;Ensure GPT has ≥2 slots per updated partition type (repart).&lt;/li&gt;
&lt;li&gt;Deploy matching &lt;code&gt;sysupdate.d&lt;/code&gt; transfers; put UKI/entry-point last alphabetically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysupdate list&lt;/code&gt; and &lt;code&gt;check-new&lt;/code&gt; on a canary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update&lt;/code&gt; on canary → &lt;code&gt;pending&lt;/code&gt; → controlled reboot → health checks.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;update timer&lt;/strong&gt; fleet-wide; keep &lt;strong&gt;reboot timer&lt;/strong&gt; tighter or manual.&lt;/li&gt;
&lt;li&gt;Keep one known-good UKI and partition set around (&lt;code&gt;InstancesMax=3&lt;/code&gt; helps) until the new version proves itself.&lt;/li&gt;
&lt;li&gt;Treat the whole stack as experimental in change control: read the man page for &lt;em&gt;your&lt;/em&gt; systemd version before copying flags from the internet.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysupdate(8)&lt;/code&gt; — commands, timers, &lt;code&gt;--image=&lt;/code&gt;, components, pending/reboot
&lt;a href="https://manpages.debian.org/unstable/systemd-container/systemd-sysupdate.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/unstable/systemd-container/systemd-sysupdate.8.en.html&lt;/a&gt;
&lt;a href="https://man7.org/linux/man-pages/man8/systemd-sysupdate.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/systemd-sysupdate.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sysupdate.d(5)&lt;/code&gt; — transfer files, resource types, match patterns, &lt;code&gt;InstancesMax=&lt;/code&gt;, verify/keyring
&lt;a href="https://manpages.debian.org/unstable/systemd-container/sysupdate.d.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/unstable/systemd-container/sysupdate.d.5.en.html&lt;/a&gt;
&lt;a href="https://man7.org/linux/man-pages/man5/sysupdate.d.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/sysupdate.d.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;UAPI Discoverable Partitions Specification (root / root-verity GPT types)
&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;UAPI Boot Loader Specification (Type #2 UKI paths under &lt;code&gt;$BOOT/EFI/Linux&lt;/code&gt;)
&lt;a href="https://uapi-group.org/specifications/specs/boot_loader_specification/" rel="noopener noreferrer"&gt;https://uapi-group.org/specifications/specs/boot_loader_specification/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-repart(8)&lt;/code&gt; — creating empty partition slots for A/B
&lt;a href="https://manpages.debian.org/unstable/systemd-repart/systemd-repart.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/unstable/systemd-repart/systemd-repart.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os-release(5)&lt;/code&gt; — &lt;code&gt;IMAGE_VERSION=&lt;/code&gt; used by &lt;code&gt;pending&lt;/code&gt; and &lt;code&gt;%A&lt;/code&gt;
&lt;a href="https://man.archlinux.org/man/os-release.5.en" rel="noopener noreferrer"&gt;https://man.archlinux.org/man/os-release.5.en&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GNU &lt;code&gt;sha256sum(1)&lt;/code&gt; — &lt;code&gt;SHA256SUMS&lt;/code&gt; manifest format
&lt;a href="https://man7.org/linux/man-pages/man1/sha256sum.1.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man1/sha256sum.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; stop treating OS updates as a long transaction against the live root. Define versioned transfers, give yourself empty slots, verify the catalog, stage beside the running system, and reboot on purpose. That is &lt;code&gt;systemd-sysupdate&lt;/code&gt;—simple HTTPS manifests, A/B (or better) slots, and activation you control.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Shipping Full Containers for Host Extensions: Practical systemd Portable Services with portablectl</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 12 Sep 2026 05:03:01 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-shipping-full-containers-for-host-extensions-practical-systemd-portable-services-with-15e0</link>
      <guid>https://dev.to/lyraalishaikh/stop-shipping-full-containers-for-host-extensions-practical-systemd-portable-services-with-15e0</guid>
      <description>&lt;h1&gt;
  
  
  Stop Shipping Full Containers for Host Extensions: Practical systemd Portable Services with portablectl
&lt;/h1&gt;

&lt;p&gt;You already know the two common answers for shipping extra software onto a Linux host:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full OS containers&lt;/strong&gt; (&lt;code&gt;systemd-nspawn&lt;/code&gt;, LXC) — their own PID 1, their own network stack, their own lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App containers&lt;/strong&gt; (Podman/Docker) — OCI images, separate runtime, often a separate mental model from &lt;code&gt;systemctl&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a third option that sits in a useful gap: &lt;strong&gt;systemd portable services&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A portable service is just an OS tree (directory, btrfs subvolume, or &lt;code&gt;.raw&lt;/code&gt; disk image) that carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the service binary and its libraries&lt;/li&gt;
&lt;li&gt;one or more systemd unit files&lt;/li&gt;
&lt;li&gt;a normal &lt;code&gt;os-release&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You &lt;strong&gt;attach&lt;/strong&gt; that image with &lt;code&gt;portablectl&lt;/code&gt;. systemd copies the matching units onto the host, pins them to &lt;code&gt;RootDirectory=&lt;/code&gt; / &lt;code&gt;RootImage=&lt;/code&gt;, and applies a &lt;strong&gt;security profile&lt;/strong&gt;. From then on the payload is a normal host unit: &lt;code&gt;systemctl start&lt;/code&gt;, journald, cgroups, timers, sockets — same tools you already use.&lt;/p&gt;

&lt;p&gt;This post is a practical operator guide: build a minimal image, attach it, pick a profile, upgrade with &lt;code&gt;reattach&lt;/code&gt;, keep state on the host, and tear it down cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What portable services are (and are not)
&lt;/h2&gt;

&lt;p&gt;From the &lt;a href="https://systemd.io/PORTABLE_SERVICES/" rel="noopener noreferrer"&gt;Portable Services design doc&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They &lt;strong&gt;do not&lt;/strong&gt; invent a new image format. Directory trees and GPT/raw images already work.&lt;/li&gt;
&lt;li&gt;They &lt;strong&gt;do not&lt;/strong&gt; run your app as PID 1. It is a normal service process under host systemd.&lt;/li&gt;
&lt;li&gt;They &lt;strong&gt;do not&lt;/strong&gt; fully isolate like Docker by default. The point is &lt;strong&gt;host integration with optional lockdown&lt;/strong&gt;, not a separate world.&lt;/li&gt;
&lt;li&gt;They &lt;strong&gt;do&lt;/strong&gt; bundle dependencies and apply stricter default sandboxing via profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Runs as&lt;/th&gt;
&lt;th&gt;Root FS&lt;/th&gt;
&lt;th&gt;Typical control plane&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host package / unit&lt;/td&gt;
&lt;td&gt;host service&lt;/td&gt;
&lt;td&gt;host root&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portable service&lt;/td&gt;
&lt;td&gt;host service&lt;/td&gt;
&lt;td&gt;image root (&lt;code&gt;RootImage=&lt;/code&gt; / &lt;code&gt;RootDirectory=&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;portablectl&lt;/code&gt; + &lt;code&gt;systemctl&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-nspawn -b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;container PID 1&lt;/td&gt;
&lt;td&gt;image root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;machinectl&lt;/code&gt; / nspawn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Podman/Docker&lt;/td&gt;
&lt;td&gt;container runtime&lt;/td&gt;
&lt;td&gt;OCI layers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;podman&lt;/code&gt; / &lt;code&gt;docker&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use portable services when the software should feel like a &lt;strong&gt;host extension&lt;/strong&gt; (agent, exporter, appliance sidecar, “super-privileged container” style workload) while still shipping its own userspace tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;systemd &lt;strong&gt;239+&lt;/strong&gt; with portable support (&lt;code&gt;portablectl&lt;/code&gt;, &lt;code&gt;systemd-portabled.service&lt;/code&gt;). On many modern distros this ships with systemd; Arch’s man page documents systemd 261-era behavior.&lt;/li&gt;
&lt;li&gt;Root (or polkit) for attach/detach.&lt;/li&gt;
&lt;li&gt;Ability to build a Linux userspace tree (&lt;code&gt;debootstrap&lt;/code&gt;, &lt;code&gt;dnf --installroot=&lt;/code&gt;, or &lt;a href="https://github.com/systemd/mkosi" rel="noopener noreferrer"&gt;mkosi&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the CLI:&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; portablectl
portablectl &lt;span class="nt"&gt;--version&lt;/span&gt;
systemctl status systemd-portabled.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;portablectl&lt;/code&gt; is missing, install/enable the portable bits for your distro (package naming varies; on some systems the binary historically lived under &lt;code&gt;/usr/lib/systemd/&lt;/code&gt; before landing on &lt;code&gt;$PATH&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Image search paths
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;portablectl list&lt;/code&gt; looks in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/var/lib/portables/&lt;/code&gt; (preferred store for large images)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/portables/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/portables/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/portables/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/portables/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommendation from &lt;code&gt;portablectl(1)&lt;/code&gt;: put real images under &lt;code&gt;/var/lib/portables/&lt;/code&gt; and only put &lt;strong&gt;symlinks&lt;/strong&gt; in &lt;code&gt;/etc/portables/&lt;/code&gt; or &lt;code&gt;/run/portables/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit name prefix rules (this trips people up)
&lt;/h2&gt;

&lt;p&gt;When you attach &lt;code&gt;foobar_47.11.raw&lt;/code&gt;, the default unit prefix is &lt;code&gt;foobar&lt;/code&gt; (filename without &lt;code&gt;.raw&lt;/code&gt;, truncated at the first &lt;code&gt;_&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Only units whose names match that prefix followed by &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;@&lt;/code&gt; are copied. Examples that match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;foobar.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;foobar-agent.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;foobar@.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;foobar.socket&lt;/code&gt; / &lt;code&gt;foobar.timer&lt;/code&gt; / &lt;code&gt;foobar.path&lt;/code&gt; / &lt;code&gt;foobar.target&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Units that do &lt;strong&gt;not&lt;/strong&gt; match the prefix are ignored. You can override prefixes on the command line after the image name.&lt;/p&gt;

&lt;p&gt;Optional hardening in the image’s &lt;code&gt;os-release&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;PORTABLE_PREFIXES=foobar
PORTABLE_SCOPE=system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PORTABLE_PREFIXES=&lt;/code&gt; documents and constrains allowed prefixes (especially useful with authenticated/verity images). &lt;code&gt;PORTABLE_SCOPE=&lt;/code&gt; can be &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, or &lt;code&gt;any&lt;/code&gt; (default implies system).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab: build a minimal directory image
&lt;/h2&gt;

&lt;p&gt;You do not need a full distro. The design doc’s minimal tree is enough for a static binary. Here is a small &lt;strong&gt;directory&lt;/strong&gt; image you can attach without building a GPT disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/portables
&lt;span class="nv"&gt;IMG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/portables/minagent_1.0.0
&lt;span class="nb"&gt;sudo 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;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/systemd/system"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/etc"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/proc"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/sys"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/dev"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/run"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/var/tmp"&lt;/span&gt;

&lt;span class="c"&gt;# Placeholder "daemon": write a heartbeat then sleep forever.&lt;/span&gt;
&lt;span class="c"&gt;# Prefer a real static binary in production; this is a lab stand-in.&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin/minagentd"&lt;/span&gt; &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;'
#!/bin/sh
echo "minagentd starting on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt; at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-Is&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
while true; do
  echo "minagentd heartbeat &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-Is&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
  sleep 30
done
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin/minagentd"&lt;/span&gt;

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/systemd/system/minagent.service"&lt;/span&gt; &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;'
[Unit]
Description=Minimal portable agent example
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/minagentd
# Writable state lives on the host, not inside a read-only image.
StateDirectory=minagent
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/usr/lib/os-release"&lt;/span&gt; &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;'
NAME="minagent portable"
ID=minagent
VERSION_ID=1.0.0
PORTABLE_PREFIXES=minagent
PORTABLE_SCOPE=system
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Required mount points / bind targets (empty is fine; host over-mounts them).&lt;/span&gt;
&lt;span class="nb"&gt;sudo&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;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/etc/resolv.conf"&lt;/span&gt;
&lt;span class="nb"&gt;sudo&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;$IMG&lt;/span&gt;&lt;span class="s2"&gt;/etc/machine-id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Important reality check for &lt;code&gt;/bin/sh&lt;/code&gt; labs
&lt;/h3&gt;

&lt;p&gt;A pure static tree is ideal. A &lt;code&gt;#!/bin/sh&lt;/code&gt; script needs a shell and dynamic loader &lt;strong&gt;inside the image&lt;/strong&gt; (or a static busybox). For a quick lab on the same distro family, clone a thin root with &lt;code&gt;debootstrap --variant=minbase&lt;/code&gt; or copy the interpreter + libs. Production images are usually built with &lt;strong&gt;mkosi&lt;/strong&gt; / installroot so the tree is self-contained.&lt;/p&gt;

&lt;p&gt;A debootstrap-shaped lab skeleton:&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;# Debian/Ubuntu-shaped example — adjust suite/mirror for your host.&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debootstrap &lt;span class="nt"&gt;--variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minbase bookworm /var/lib/portables/minagent_1.0.0 http://deb.debian.org/debian
&lt;span class="c"&gt;# Then install your unit + binary paths as above, and set os-release PORTABLE_* fields.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or follow the official walkthrough repo that builds a C daemon with mkosi: &lt;a href="https://github.com/systemd/portable-walkthrough" rel="noopener noreferrer"&gt;systemd/portable-walkthrough&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect before you attach
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl inspect /var/lib/portables/minagent_1.0.0
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl inspect &lt;span class="nt"&gt;--cat&lt;/span&gt; /var/lib/portables/minagent_1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;os-release&lt;/code&gt; metadata and the matching unit list (&lt;code&gt;minagent.service&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach, enable, start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Persistent attach (units under /etc/systemd/system.attached/)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl attach &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="nt"&gt;--enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; /var/lib/portables/minagent_1.0.0

&lt;span class="c"&gt;# Or temporary until reboot:&lt;/span&gt;
&lt;span class="c"&gt;# sudo portablectl attach --runtime --profile=default --now /var/lib/portables/minagent_1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What attach does (&lt;code&gt;portablectl(1)&lt;/code&gt; / design doc):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copies matching &lt;code&gt;.service&lt;/code&gt; / &lt;code&gt;.socket&lt;/code&gt; / &lt;code&gt;.target&lt;/code&gt; / &lt;code&gt;.timer&lt;/code&gt; / &lt;code&gt;.path&lt;/code&gt; units into &lt;code&gt;/etc/systemd/system.attached/&lt;/code&gt; (or &lt;code&gt;/run/...&lt;/code&gt; with &lt;code&gt;--runtime&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Writes a drop-in (&lt;code&gt;20-portable.conf&lt;/code&gt;) with &lt;code&gt;RootDirectory=&lt;/code&gt; or &lt;code&gt;RootImage=&lt;/code&gt;, plus &lt;code&gt;Environment=PORTABLE=...&lt;/code&gt; and &lt;code&gt;LogExtraFields=PORTABLE=...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Links a &lt;strong&gt;profile&lt;/strong&gt; drop-in (&lt;code&gt;10-profile.conf&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Symlinks the image into the portable search path if needed.&lt;/li&gt;
&lt;li&gt;Reloads the manager (unless &lt;code&gt;--no-reload&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify:&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;portablectl is-attached minagent_1.0.0
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl list
systemctl status minagent.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;minagent.service
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/systemd/system.attached/minagent.service.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected attachment states include: &lt;code&gt;detached&lt;/code&gt;, &lt;code&gt;attached&lt;/code&gt;, &lt;code&gt;attached-runtime&lt;/code&gt;, &lt;code&gt;enabled&lt;/code&gt;, &lt;code&gt;enabled-runtime&lt;/code&gt;, &lt;code&gt;running&lt;/code&gt;, &lt;code&gt;running-runtime&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiles: the real product feature
&lt;/h2&gt;

&lt;p&gt;Profiles are host-local drop-ins under:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/systemd/portable/profile/&amp;lt;name&amp;gt;/service.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/systemd/portable/profile/&amp;lt;name&amp;gt;/&lt;/code&gt; for custom profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shipped profiles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Medium lockdown; journal + D-Bus + IP networking allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nonetwork&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like default, but &lt;code&gt;PrivateNetwork=yes&lt;/code&gt; and &lt;code&gt;IPAddressDeny=any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tightest stock profile: no caps, AF_UNIX only, no net, low &lt;code&gt;TasksMax&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trusted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimal restrictions; effectively full host trust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Select at attach time:&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;portablectl attach &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;strict /var/lib/portables/minagent_1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excerpt from the stock &lt;strong&gt;default&lt;/strong&gt; profile (systemd v256 tree):&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;[Service]&lt;/span&gt;
&lt;span class="py"&gt;MountAPIVFS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;BindReadOnlyPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/dev/log /run/systemd/journal/socket /run/systemd/journal/stdout&lt;/span&gt;
&lt;span class="py"&gt;BindReadOnlyPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/machine-id&lt;/span&gt;
&lt;span class="py"&gt;BindReadOnlyPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/etc/resolv.conf&lt;/span&gt;
&lt;span class="py"&gt;BindReadOnlyPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/dbus/system_bus_socket&lt;/span&gt;
&lt;span class="py"&gt;DynamicUser&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateDevices&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateUsers&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&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;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectKernelTunables&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectKernelModules&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectControlGroups&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;RestrictAddressFamilies&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AF_UNIX AF_NETLINK AF_INET AF_INET6&lt;/span&gt;
&lt;span class="py"&gt;MemoryDenyWriteExecute&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;RestrictRealtime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;RestrictNamespaces&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;SystemCallFilter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;@system-service&lt;/span&gt;
&lt;span class="py"&gt;SystemCallArchitectures&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;native&lt;/span&gt;
&lt;span class="c"&gt;# plus a reduced CapabilityBoundingSet= ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excerpt from &lt;strong&gt;strict&lt;/strong&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="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;CapabilityBoundingSet&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;RestrictAddressFamilies&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AF_UNIX&lt;/span&gt;
&lt;span class="py"&gt;PrivateNetwork&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;IPAddressDeny&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;any&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;span class="py"&gt;TasksMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="c"&gt;# ... same Protect*/Private* baseline as default ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operator rule: &lt;strong&gt;the image vendor ships code; the host admin chooses the profile&lt;/strong&gt;. That split is intentional.&lt;/p&gt;

&lt;p&gt;Need host files or sockets? Use normal unit directives in the image unit (or a host drop-in via &lt;code&gt;systemctl edit&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="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;BindReadOnlyPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/myservice.sock&lt;/span&gt;
&lt;span class="py"&gt;BindPaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/minagent-extra&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Writable data with immutable images
&lt;/h2&gt;

&lt;p&gt;All profiles except &lt;code&gt;trusted&lt;/code&gt; lean on &lt;code&gt;ProtectSystem=strict&lt;/code&gt;. Keep the image read-only and put mutable data on the host with &lt;code&gt;StateDirectory=&lt;/code&gt; / &lt;code&gt;CacheDirectory=&lt;/code&gt; / &lt;code&gt;LogsDirectory=&lt;/code&gt; / &lt;code&gt;RuntimeDirectory=&lt;/code&gt; in the 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="c"&gt;# After the unit has run once under DynamicUser/StateDirectory:&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/lib/minagent
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; minagent.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;Optional:&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;portablectl read-only minagent_1.0.0 &lt;span class="nb"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Upgrade with less downtime: &lt;code&gt;reattach&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Ship a new version as &lt;code&gt;minagent_1.0.1&lt;/code&gt; (underscore versioning). &lt;code&gt;reattach&lt;/code&gt; detaches the old attachment and attaches the new image; with &lt;code&gt;--now&lt;/code&gt; it &lt;strong&gt;restarts&lt;/strong&gt; updated units instead of a blunt stop+start cycle, which helps preserve runtime state such as the file descriptor store when applicable.&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;# Build/copy new tree or .raw named minagent_1.0.1&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl reattach &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="nt"&gt;--now&lt;/span&gt; /var/lib/portables/minagent_1.0.1
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl is-attached minagent_1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Partial matching: only the part before the first &lt;code&gt;_&lt;/code&gt; must match for upgrade pairing.&lt;/p&gt;

&lt;p&gt;Keep the image path stable while attached. Moving the file breaks &lt;code&gt;RootImage=&lt;/code&gt; / &lt;code&gt;RootDirectory=&lt;/code&gt; in the generated drop-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensions (sysext/confext layering)
&lt;/h2&gt;

&lt;p&gt;Since v249, &lt;code&gt;portablectl attach --extension PATH ...&lt;/code&gt; can stack OverlayFS layers: a shared base runtime image plus thin app extensions. Extension images need matching &lt;code&gt;extension-release&lt;/code&gt; metadata (&lt;code&gt;ID=&lt;/code&gt; + &lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt;/&lt;code&gt;VERSION_ID=&lt;/code&gt; or confext equivalents). Same extensions, same order, are required on detach.&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;# Conceptual pattern from the design doc:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl attach &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extension&lt;/span&gt; foobar_0.7.23.raw &lt;span class="se"&gt;\&lt;/span&gt;
  debian-runtime_11.1.raw &lt;span class="se"&gt;\&lt;/span&gt;
  foobar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when many agents share one distro runtime and you only want to ship the app layer repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging fields you can query
&lt;/h2&gt;

&lt;p&gt;Portable attach injects structured journal fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PORTABLE=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PORTABLE_NAME_AND_VERSION=&lt;/code&gt; (from &lt;code&gt;IMAGE_ID&lt;/code&gt;/&lt;code&gt;ID&lt;/code&gt; + &lt;code&gt;IMAGE_VERSION&lt;/code&gt;/&lt;code&gt;VERSION_ID&lt;/code&gt;/&lt;code&gt;BUILD_ID&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;with extensions: &lt;code&gt;PORTABLE_ROOT=&lt;/code&gt;, &lt;code&gt;PORTABLE_EXTENSION=&lt;/code&gt;, &lt;code&gt;PORTABLE_*_NAME_AND_VERSION=&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nv"&gt;FIELD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;PORTABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minagent_1.0.0 &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# or filter by unit as usual&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; minagent.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Instantiation
&lt;/h2&gt;

&lt;p&gt;No special portable API: ship &lt;code&gt;foobar@.service&lt;/code&gt; and instantiate after attach.&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;portablectl attach foobar_0.7.23.raw
&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; foobar@edge-a.service
&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; foobar@edge-b.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Detach and clean up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop + disable + detach&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl detach &lt;span class="nt"&gt;--enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; minagent_1.0.0

&lt;span class="c"&gt;# systemd v256+: also remove host state/logs/cache/runtime dirs for the service&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl detach &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; minagent_1.0.0

&lt;span class="c"&gt;# Remove only a search-path symlink/image entry (does not follow and delete the target if it is a symlink)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;portablectl remove minagent_1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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;portablectl is-attached minagent_1.0.0   &lt;span class="c"&gt;# detached&lt;/span&gt;
systemctl status minagent.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;  &lt;span class="c"&gt;# not-found / inactive as expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Copy mode and image policy notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--copy=copy|symlink|auto|mixed&lt;/code&gt; controls whether attached units/profiles/images are copied or symlinked. Raw disk images force copy when symlink is impossible. &lt;code&gt;mixed&lt;/code&gt; (v256+) symlinks profile drop-ins but copies units/images.&lt;/li&gt;
&lt;li&gt;On attach, systemd can generate an &lt;strong&gt;image policy&lt;/strong&gt; that pins the content discovered at attach time so a later swap cannot silently drop protections such as dm-verity without reinstall. See &lt;code&gt;systemd.image-policy(7)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Build a self-contained tree or &lt;code&gt;.raw&lt;/code&gt; with matching unit prefix + &lt;code&gt;os-release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;portablectl inspect&lt;/code&gt; before attach.&lt;/li&gt;
&lt;li&gt;Choose profile deliberately (&lt;code&gt;default&lt;/code&gt; / &lt;code&gt;nonetwork&lt;/code&gt; / &lt;code&gt;strict&lt;/code&gt; / custom / &lt;code&gt;trusted&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;StateDirectory=&lt;/code&gt; over writable images.&lt;/li&gt;
&lt;li&gt;Store images under &lt;code&gt;/var/lib/portables/&lt;/code&gt; and version with &lt;code&gt;_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Upgrade via &lt;code&gt;reattach --now&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tear down with &lt;code&gt;detach --now&lt;/code&gt; and &lt;code&gt;--clean&lt;/code&gt; when you want host state gone.&lt;/li&gt;
&lt;li&gt;Treat portable units like any other units for cgroup limits: &lt;code&gt;systemctl set-property minagent.service MemoryMax=512M CPUQuota=50%&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Boundaries (so this does not blur into other tools)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a replacement for Podman Quadlet app containers when you need OCI registries, rootless stacks, and Kubernetes-shaped workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a replacement for &lt;code&gt;systemd-nspawn -b&lt;/code&gt; when you need a full guest OS with its own init and machine lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a substitute for whole-disk integrity (&lt;code&gt;dm-verity&lt;/code&gt;) or per-file authenticity (&lt;code&gt;fs-verity&lt;/code&gt;); those compose &lt;em&gt;with&lt;/em&gt; portable images when you ship &lt;code&gt;.raw&lt;/code&gt; + verity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; soft-reboot / image A/B host update logic; portable services extend a live host, they do not replace host OS update strategy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://systemd.io/PORTABLE_SERVICES/" rel="noopener noreferrer"&gt;Portable Services Introduction (systemd.io)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.archlinux.org/man/portablectl.1.en" rel="noopener noreferrer"&gt;portablectl(1) — Arch man pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.archlinux.org/man/systemd-portabled.service.8.en" rel="noopener noreferrer"&gt;systemd-portabled.service(8)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://0pointer.net/blog/walkthrough-for-portable-services.html" rel="noopener noreferrer"&gt;Walkthrough for Portable Services — Lennart Poettering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/systemd/portable-walkthrough" rel="noopener noreferrer"&gt;systemd/portable-walkthrough example repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Stock profiles in systemd source: &lt;a href="https://raw.githubusercontent.com/systemd/systemd/v256/src/portable/profile/default/service.conf" rel="noopener noreferrer"&gt;&lt;code&gt;default/service.conf&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://raw.githubusercontent.com/systemd/systemd/v256/src/portable/profile/strict/service.conf" rel="noopener noreferrer"&gt;&lt;code&gt;strict/service.conf&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://raw.githubusercontent.com/systemd/systemd/v256/src/portable/profile/nonetwork/service.conf" rel="noopener noreferrer"&gt;&lt;code&gt;nonetwork/service.conf&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related: &lt;a href="https://github.com/systemd/mkosi" rel="noopener noreferrer"&gt;mkosi&lt;/a&gt;, &lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification" rel="noopener noreferrer"&gt;Discoverable Partitions Specification&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Portable services are the “missing middle” for Linux operators who want &lt;strong&gt;bundled dependencies&lt;/strong&gt; and &lt;strong&gt;profile-based sandboxing&lt;/strong&gt; without leaving the systemd control plane. Build an image, &lt;code&gt;portablectl attach --profile=... --enable --now&lt;/code&gt;, manage it with &lt;code&gt;systemctl&lt;/code&gt; and journald, upgrade with &lt;code&gt;reattach&lt;/code&gt;, and detach without scattering random unit files by hand.&lt;/p&gt;

&lt;p&gt;If your next component is “basically a host service, but I refuse to pollute the host rootfs with its library tree,” try a portable image before you reach for a full guest OS container.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
