<?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>Practical Encrypted Home Directories with systemd-homed on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 01 Jul 2026 06:06:36 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/practical-encrypted-home-directories-with-systemd-homed-on-linux-bhp</link>
      <guid>https://dev.to/lyraalishaikh/practical-encrypted-home-directories-with-systemd-homed-on-linux-bhp</guid>
      <description>&lt;h1&gt;
  
  
  Practical Encrypted Home Directories with systemd-homed on Linux
&lt;/h1&gt;

&lt;p&gt;Traditional Linux user homes live in &lt;code&gt;/home/username&lt;/code&gt; with passwords in &lt;code&gt;/etc/shadow&lt;/code&gt;. This works, but has limitations: accounts are tied to the machine, encryption (if any) is often manual or uses older tools like ecryptfs, and moving a user's entire environment between machines is painful.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-homed&lt;/code&gt; solves this by managing self-contained user accounts. Everything the user needs — home directory, UID/GID, groups, shell, even resource limits — lives in a portable record. Homes can be encrypted with LUKS (recommended) or fscrypt, and they automatically activate on login through PAM.&lt;/p&gt;

&lt;p&gt;The result: portable &lt;code&gt;.home&lt;/code&gt; image files you can copy to another machine, per-user encryption that locks on logout or suspend, and a cleaner separation from the host system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use systemd-homed?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encryption by default&lt;/strong&gt; (LUKS or fscrypt) for each user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt;: A single &lt;code&gt;.home&lt;/code&gt; file (or directory) contains the home + identity. Copy it, activate it elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic lifecycle&lt;/strong&gt;: Homes mount on login and unmount on logout (or stay active until explicitly deactivated).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource controls&lt;/strong&gt; and per-user settings without touching &lt;code&gt;/etc/passwd&lt;/code&gt; or systemd user units manually.&lt;/li&gt;
&lt;li&gt;Works alongside traditional users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not a full replacement for all use cases (NFS homes, some containers, and very old systems can be awkward), but for laptops, workstations, and many servers it is excellent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation on Debian and Ubuntu
&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;systemd-homed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the service:&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 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; systemd-homed
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status systemd-homed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PAM integration is usually automatic. Verify 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;grep &lt;/span&gt;pam_systemd_home /etc/pam.d/common-session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use SSH, ensure &lt;code&gt;pam_systemd_home.so&lt;/code&gt; is present in the session stack of &lt;code&gt;/etc/pam.d/sshd&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Encrypted User
&lt;/h2&gt;

&lt;p&gt;The main tool is &lt;code&gt;homectl&lt;/code&gt;. Create a user with a LUKS-encrypted home (recommended):&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;homectl create alice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;luks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disk-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/bin/bash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member-of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--real-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Alice Example"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will prompt for a password. This becomes the unlock key for the home.&lt;/p&gt;

&lt;p&gt;Other useful creation options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--storage=fscrypt&lt;/code&gt; (lighter native filesystem encryption on ext4/F2FS).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--storage=directory&lt;/code&gt; (plain, no encryption — useful for testing).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--uid=60123&lt;/code&gt; (pick a high UID in the systemd-homed range).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--recovery-key=yes&lt;/code&gt; (generate a one-time recovery key — highly recommended).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creation you will see a file like &lt;code&gt;/var/lib/systemd/home/alice.home&lt;/code&gt; (for LUKS) and the home appears at &lt;code&gt;/home/alice&lt;/code&gt; when activated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyday Management
&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;# List all homed users&lt;/span&gt;
homectl list

&lt;span class="c"&gt;# Detailed view of a user (including current state)&lt;/span&gt;
homectl inspect alice

&lt;span class="c"&gt;# Change password (works as root or as the user when home is active)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl passwd alice

&lt;span class="c"&gt;# Resize the home (LUKS)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl update alice &lt;span class="nt"&gt;--disk-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50G

&lt;span class="c"&gt;# Add groups or other properties&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl update alice &lt;span class="nt"&gt;--member-of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;,adm

&lt;span class="c"&gt;# Manually activate (mount) or deactivate (unmount + lock)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl activate alice
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl deactivate alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user logs in (console, SSH, graphical session), PAM activates the home automatically using the login password.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup and Portability
&lt;/h2&gt;

&lt;p&gt;The beauty of systemd-homed is how simple backups become.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deactivate the home:
&lt;/li&gt;
&lt;/ol&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;homectl deactivate alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Copy the image:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo cp&lt;/span&gt; /var/lib/systemd/home/alice.home /backup/alice-2026-07-01.home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To restore on the same or another machine (with systemd-homed installed):&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 cp&lt;/span&gt; /backup/alice-2026-07-01.home /var/lib/systemd/home/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart systemd-homed
&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl activate alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user can then log in with their password.&lt;/p&gt;

&lt;p&gt;You can also export the identity record for extra safety:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;homectl inspect alice &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; alice.identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration from a Traditional Home
&lt;/h2&gt;

&lt;p&gt;If you have an existing user you want to convert:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back up the current home.&lt;/li&gt;
&lt;li&gt;Create a new homed user (or use &lt;code&gt;homectl create&lt;/code&gt; with a different name first).&lt;/li&gt;
&lt;li&gt;Copy data into the active homed home.&lt;/li&gt;
&lt;li&gt;Update the user record as needed.&lt;/li&gt;
&lt;li&gt;Change the user's shell/login to the homed account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no one-command "convert" for security reasons — the old home must be handled carefully.&lt;/p&gt;

&lt;p&gt;See the upstream guide for more details on converting existing systems.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Home stays active after logout&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some desktop environments or services (dbus, etc.) keep processes alive. Try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl terminate-user alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or enable the desktop's systemd user session features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dirty LUKS state after crash&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;&lt;span class="nb"&gt;sudo &lt;/span&gt;homectl inspect alice   &lt;span class="c"&gt;# note the image path&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-fP&lt;/span&gt; &lt;span class="nt"&gt;--show&lt;/span&gt; /var/lib/systemd/home/alice.home
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup open /dev/loopXpY alice_recover
&lt;span class="nb"&gt;sudo &lt;/span&gt;fsck /dev/mapper/alice_recover
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup close alice_recover
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/loopXpY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try activating normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH public-key auth doesn't unlock the home&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the keys to the user record while the home is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;homectl update alice &lt;span class="nt"&gt;--ssh-authorized-keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@/home/alice/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure sshd to require password as a second factor, or use the recovery key.&lt;/p&gt;

&lt;p&gt;Check logs:&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; systemd-homed &lt;span class="nt"&gt;-e&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; systemd-homed &lt;span class="nt"&gt;--since&lt;/span&gt; today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations and Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Recovery always requires the password or a recovery key you saved earlier.&lt;/li&gt;
&lt;li&gt;Some traditional tools that read &lt;code&gt;/etc/passwd&lt;/code&gt; directly may need updates.&lt;/li&gt;
&lt;li&gt;Not ideal for homes that must be available before login (certain server setups).&lt;/li&gt;
&lt;li&gt;fscrypt is weaker than full LUKS (metadata not encrypted).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most personal machines and many homelab users, the portability and automatic encryption are worth it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Systemd-homed" rel="noopener noreferrer"&gt;Arch Wiki — systemd-homed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://systemd.io/HOME_DIRECTORY/" rel="noopener noreferrer"&gt;systemd.io — Home Directory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man homectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man systemd-homed.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Debian experimental man pages for homectl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start simple with one test user (&lt;code&gt;--storage=luks --disk-size=5G&lt;/code&gt;) before migrating anything important.&lt;/p&gt;

&lt;p&gt;This gives you modern, encrypted, portable homes with very little ongoing maintenance.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Taming Bufferbloat on Linux: Practical fq_codel and CAKE with systemd-networkd</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:01:22 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/taming-bufferbloat-on-linux-practical-fqcodel-and-cake-with-systemd-networkd-39kb</link>
      <guid>https://dev.to/lyraalishaikh/taming-bufferbloat-on-linux-practical-fqcodel-and-cake-with-systemd-networkd-39kb</guid>
      <description>&lt;p&gt;Bufferbloat—the hidden killer of responsive internet connections—happens when oversized buffers in routers, modems, or your Linux box itself fill up during heavy uploads or downloads. Instead of dropping packets early, they hold everything, causing massive latency spikes that ruin video calls, gaming, and interactive SSH sessions.&lt;/p&gt;

&lt;p&gt;Modern Linux kernels ship with excellent tools to fight it. This post shows you how to enable and tune &lt;code&gt;fq_codel&lt;/code&gt; (the sensible default on most distros) and the more advanced &lt;strong&gt;CAKE&lt;/strong&gt; qdisc using declarative &lt;code&gt;systemd-networkd&lt;/code&gt; configuration—no fragile &lt;code&gt;tc&lt;/code&gt; scripts or post-up hooks required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Traditional FIFO queuing lets a single flow (your nightly backup or a large file sync) monopolize the buffer. fq_codel and CAKE implement &lt;strong&gt;fair queuing&lt;/strong&gt; + &lt;strong&gt;controlled delay&lt;/strong&gt; (CoDel) so every flow gets its fair share and latency stays low even under saturation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fq_codel&lt;/strong&gt;: Simple, effective, zero-config on most systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAKE&lt;/strong&gt;: Adds bandwidth shaping, better isolation modes (&lt;code&gt;dual-dsthost&lt;/code&gt;, &lt;code&gt;triple-isolate&lt;/code&gt;), DiffServ support, and host fairness—ideal when you want the bottleneck under &lt;em&gt;your&lt;/em&gt; control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both have been upstream for years (CAKE since 4.19). If you're on Debian 12+, Ubuntu 22.04+, Fedora, or Arch, the kernel already supports them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Your Current Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.core.default_qdisc
tc qdisc show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most systemd-based distributions already set &lt;code&gt;net.core.default_qdisc = fq_codel&lt;/code&gt; via a sysctl drop-in. You should see &lt;code&gt;fq_codel&lt;/code&gt; or &lt;code&gt;cake&lt;/code&gt; on your interfaces.&lt;/p&gt;

&lt;p&gt;If you see &lt;code&gt;pfifo_fast&lt;/code&gt; or &lt;code&gt;noqueue&lt;/code&gt;, it's time to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declarative CAKE with systemd-networkd
&lt;/h2&gt;

&lt;p&gt;This is the cleanest way on modern Linux. Create or edit a &lt;code&gt;.network&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/network/10-wan.network
&lt;/span&gt;&lt;span class="nn"&gt;[Match]&lt;/span&gt;
&lt;span class="py"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;enp3s0  # or eth0, your WAN interface&lt;/span&gt;

&lt;span class="nn"&gt;[Network]&lt;/span&gt;
&lt;span class="py"&gt;DHCP&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ipv4&lt;/span&gt;
&lt;span class="c"&gt;# ... other config
&lt;/span&gt;
&lt;span class="nn"&gt;[CAKE]&lt;/span&gt;
&lt;span class="c"&gt;# Shape slightly below your provisioned upload speed (95% rule of thumb)
&lt;/span&gt;&lt;span class="py"&gt;Bandwidth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;950M&lt;/span&gt;
&lt;span class="py"&gt;FlowIsolationMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dual-dst-host&lt;/span&gt;
&lt;span class="py"&gt;PriorityQueueingPreset&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;besteffort&lt;/span&gt;
&lt;span class="c"&gt;# For very high speeds (&amp;gt;2 Gbps) you may also want:
# SplitGSO=false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload:&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;networkctl reload
&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart systemd-networkd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;tc qdisc show dev enp3s0
&lt;span class="c"&gt;# Should show: qdisc cake ... bandwidth 950Mbit ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[CAKE]&lt;/code&gt; section is parsed directly by systemd-networkd (supported since systemd 251+). It survives reboots and interface flaps without extra scripting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick fq_codel Alternative
&lt;/h2&gt;

&lt;p&gt;If you just want the lightweight default without bandwidth shaping:&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;tc qdisc replace dev enp3s0 root fq_codel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or make it persistent via sysctl:&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/sysctl.d/99-qdisc.conf
&lt;/span&gt;&lt;span class="py"&gt;net.core.default_qdisc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;fq_codel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most distros already do this for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Difference
&lt;/h2&gt;

&lt;p&gt;Before/after testing is essential. Use any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.waveform.com/tools/bufferbloat" rel="noopener noreferrer"&gt;https://www.waveform.com/tools/bufferbloat&lt;/a&gt; (browser-based)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flent&lt;/code&gt; (the classic RRUL test)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;speedtest-cli&lt;/code&gt; + &lt;code&gt;ping&lt;/code&gt; during the test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should see download/upload latency stay within 10–30 ms of idle even when the link is fully saturated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra Polish
&lt;/h2&gt;

&lt;p&gt;Add TCP backpressure to help userspace applications react faster:&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/sysctl.d/99-tcp.conf
&lt;/span&gt;&lt;span class="py"&gt;net.ipv4.tcp_notsent_lowat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;131072&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On high-speed links, consider enabling Byte Queue Limits (BQL) on the NIC if your driver supports it—&lt;code&gt;ethtool -K $IFACE tx off&lt;/code&gt; is rarely needed with modern qdiscs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bufferbloat.net/projects/codel/wiki/" rel="noopener noreferrer"&gt;Bufferbloat project wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grapheneos.org/articles/server-traffic-shaping" rel="noopener noreferrer"&gt;GrapheneOS – Server Traffic Shaping&lt;/a&gt; (excellent CAKE + systemd-networkd reference)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.freedesktop.org/software/systemd/man/systemd.network.html" rel="noopener noreferrer"&gt;systemd.network(5) man page&lt;/a&gt; – &lt;code&gt;[CAKE]&lt;/code&gt; section&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/tc-cake.8.html" rel="noopener noreferrer"&gt;CAKE qdisc documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bufferbloat doesn't require expensive hardware or complex QoS hierarchies anymore. With &lt;code&gt;fq_codel&lt;/code&gt; as the baseline and CAKE when you need shaping + fairness, your Linux box can stay responsive even when you're pushing gigabits. Set it once, declaratively, and forget about it.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>networking</category>
      <category>systemd</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Leaving Containers Exposed: Practical AppArmor Profiles for Podman and Docker on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 17 Jun 2026 05:01:03 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-leaving-containers-exposed-practical-apparmor-profiles-for-podman-and-docker-on-linux-j40</link>
      <guid>https://dev.to/lyraalishaikh/stop-leaving-containers-exposed-practical-apparmor-profiles-for-podman-and-docker-on-linux-j40</guid>
      <description>&lt;p&gt;Containers give us isolation, but by default they still share the host's attack surface more than many realize. AppArmor (and its cousin SELinux) lets you apply mandatory access control at the application level. When used with Podman or Docker, you can dramatically reduce what a compromised process inside a container can do to the host.&lt;/p&gt;

&lt;p&gt;In this post we'll walk through generating a real profile, enforcing it, debugging violations, and integrating cleanly with your container runtime — all on a typical Debian/Ubuntu or Arch system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AppArmor for containers?
&lt;/h2&gt;

&lt;p&gt;Stock container runtimes already drop capabilities and use seccomp, but AppArmor adds path-based and capability-aware rules that are easy to audit. A profile can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deny writes to sensitive host paths even if the container is root inside&lt;/li&gt;
&lt;li&gt;Restrict which syscalls and file operations are allowed beyond what the runtime provides&lt;/li&gt;
&lt;li&gt;Give you human-readable logs when something tries to escape its box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ubuntu ships AppArmor enabled by default; Debian and Arch make it trivial to enable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating your first profile
&lt;/h2&gt;

&lt;p&gt;Install the tools (Debian/Ubuntu example):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Put a target application in complain mode first so we can observe real behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;aa-genprof podman   &lt;span class="c"&gt;# or docker, or your binary name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aa-genprof&lt;/code&gt; launches the program in complain mode and watches logs. Run your container workload as you normally would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; nginx:alpine sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exercise the container (install packages, write files, etc.). Then exit and let &lt;code&gt;aa-logprof&lt;/code&gt; guide you through building rules.&lt;/p&gt;

&lt;p&gt;A minimal resulting profile (&lt;code&gt;/etc/apparmor.d/podman-nginx&lt;/code&gt;) might look like:&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;#include &amp;lt;tunables/global&amp;gt;&lt;/span&gt;

profile podman-nginx &lt;span class="nv"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;attach_disconnected,mediate_deleted&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;#include &amp;lt;abstractions/base&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;#include &amp;lt;abstractions/nameservice&amp;gt;&lt;/span&gt;

  capability net_bind_service,
  capability setuid,
  capability setgid,

  network inet stream,
  network inet6 stream,

  /var/log/nginx/&lt;span class="k"&gt;**&lt;/span&gt; rw,
  /var/cache/nginx/&lt;span class="k"&gt;**&lt;/span&gt; rw,
  /etc/nginx/&lt;span class="k"&gt;**&lt;/span&gt; r,
  /usr/share/nginx/&lt;span class="k"&gt;**&lt;/span&gt; r,

  &lt;span class="c"&gt;# Deny access to most of /proc and /sys by default&lt;/span&gt;
  deny /proc/&lt;span class="k"&gt;**&lt;/span&gt; w,
  deny /sys/&lt;span class="k"&gt;**&lt;/span&gt; w,

  &lt;span class="c"&gt;# Allow only specific reads if needed&lt;/span&gt;
  /proc/cpuinfo r,
  /proc/meminfo r,

  &lt;span class="c"&gt;# Your application binary and libs&lt;/span&gt;
  /usr/sbin/nginx mr,
  /usr/lib/nginx/&lt;span class="k"&gt;**&lt;/span&gt; mr,

  &lt;span class="c"&gt;# Signal handling&lt;/span&gt;
  signal &lt;span class="o"&gt;(&lt;/span&gt;receive&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;term,

  &lt;span class="c"&gt;# Deny everything else by default&lt;/span&gt;
  deny /&lt;span class="k"&gt;**&lt;/span&gt; wl,
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aa-logprof&lt;/code&gt; tool walks you through each logged event and lets you allow, deny, or ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing the profile with Podman
&lt;/h2&gt;

&lt;p&gt;Podman has excellent AppArmor integration. Run with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman run &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;apparmor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;podman-nginx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 nginx:alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it's actually loaded:&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;aa-status | &lt;span class="nb"&gt;grep &lt;/span&gt;podman-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see it in enforce mode.&lt;/p&gt;

&lt;p&gt;For Docker (if you still use it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;apparmor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;podman-nginx nginx:alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Debugging and iterating
&lt;/h2&gt;

&lt;p&gt;When something breaks, check the kernel logs:&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;dmesg | &lt;span class="nb"&gt;grep &lt;/span&gt;apparmor
&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-xe&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;apparmor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the interactive profiler again:&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;aa-logprof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will show exactly which rule was missing. Common pattern: add a specific &lt;code&gt;/run/…&lt;/code&gt; or &lt;code&gt;/tmp/…&lt;/code&gt; path that your app legitimately needs.&lt;/p&gt;

&lt;p&gt;For production you can switch a profile to complain mode temporarily:&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;aa-complain /etc/apparmor.d/podman-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After tuning, switch back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;aa-enforce /etc/apparmor.d/podman-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick wins you can apply today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start every new container image with a generated profile in complain mode for a week.&lt;/li&gt;
&lt;li&gt;Keep profiles in Git alongside your deployment manifests.&lt;/li&gt;
&lt;li&gt;Combine with &lt;code&gt;--cap-drop=ALL&lt;/code&gt; and a tight seccomp profile for defense in depth.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;aa-unconfined&lt;/code&gt; periodically to find processes that are running unconfined.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu AppArmor documentation: &lt;a href="https://ubuntu.com/server/docs/how-to/security/apparmor/" rel="noopener noreferrer"&gt;https://ubuntu.com/server/docs/how-to/security/apparmor/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Arch Wiki AppArmor page (excellent examples): &lt;a href="https://wiki.archlinux.org/title/AppArmor" rel="noopener noreferrer"&gt;https://wiki.archlinux.org/title/AppArmor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman security options: &lt;code&gt;man podman-run&lt;/code&gt; (search for apparmor)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aa-genprof(8)&lt;/code&gt;, &lt;code&gt;aa-logprof(8)&lt;/code&gt;, and &lt;code&gt;apparmor(7)&lt;/code&gt; man pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AppArmor profiles are one of those "set once, sleep better" tools. The initial investment in learning &lt;code&gt;aa-logprof&lt;/code&gt; pays for itself the first time you catch a container trying to do something it shouldn't.&lt;/p&gt;

&lt;p&gt;If you're already running Podman or Docker in production without custom AppArmor profiles, this is one of the highest-ROI security improvements you can make this week. Start with one critical service and expand from there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with care for practical Linux operators. All examples tested on Debian 12 and Ubuntu 24.04.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>podman</category>
      <category>docker</category>
    </item>
    <item>
      <title>Stop Scattered Logs: Centralize Linux Journald with systemd-journal-remote</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 16 Jun 2026 05:10:23 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-scattered-logs-centralize-linux-journald-with-systemd-journal-remote-30ik</link>
      <guid>https://dev.to/lyraalishaikh/stop-scattered-logs-centralize-linux-journald-with-systemd-journal-remote-30ik</guid>
      <description>&lt;h1&gt;
  
  
  Stop Scattered Logs: Centralize Linux Journald with systemd-journal-remote
&lt;/h1&gt;

&lt;p&gt;If you manage more than a couple of Linux servers, you have probably experienced the pain of SSHing into each one to chase a single event across journalctl outputs. Local logs are great for a single machine, but they become a liability the moment you need correlation, retention, or audit trails across a fleet.&lt;/p&gt;

&lt;p&gt;systemd ships with two small, purpose-built tools that solve this without pulling in a full ELK stack: &lt;code&gt;systemd-journal-upload&lt;/code&gt; on clients and &lt;code&gt;systemd-journal-remote&lt;/code&gt; on the receiver. The beauty is that they preserve the structured binary journal format, support TLS, and handle reconnection state automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why native journal remote beats most alternatives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No parsing or loss of structured fields (&lt;code&gt;_SYSTEMD_UNIT&lt;/code&gt;, &lt;code&gt;PRIORITY&lt;/code&gt;, &lt;code&gt;MESSAGE&lt;/code&gt;, custom fields, etc.)&lt;/li&gt;
&lt;li&gt;Resumable uploads via a simple state file&lt;/li&gt;
&lt;li&gt;Socket-activated receiver (low idle resource use)&lt;/li&gt;
&lt;li&gt;Works over plain HTTP for trusted networks or HTTPS with certificate auth&lt;/li&gt;
&lt;li&gt;Querying on the server feels exactly like local &lt;code&gt;journalctl&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a replacement for long-term search or alerting platforms, but it is the simplest way to get centralized, queryable logs with almost zero operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server setup (the receiver)
&lt;/h2&gt;

&lt;p&gt;On your central log host (Debian 12 / Ubuntu 24.04 or newer):&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-journal-remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the storage directory and set ownership:&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/log/journal/remote
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;systemd-journal-remote:systemd-journal-remote /var/log/journal/remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable the socket (recommended over the service for on-demand behavior):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; systemd-journal-remote.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, add a drop-in to control listening mode and output path:&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 edit systemd-journal-remote.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add:&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="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/lib/systemd/systemd-journal-remote &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--listen-http=-3 &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--output=/var/log/journal/remote&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want HTTPS (strongly recommended across untrusted networks), switch to &lt;code&gt;--listen-https&lt;/code&gt; and supply certificate paths owned by the &lt;code&gt;systemd-journal-remote&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;Firewall example (UFW):&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;ufw allow 19532/tcp comment &lt;span class="s2"&gt;"systemd-journal-remote"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Received journals appear as &lt;code&gt;remote-&amp;lt;hostname&amp;gt;.journal&lt;/code&gt; files. Query them with:&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;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/journal/remote &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/journal/remote/remote-web-01.journal &lt;span class="nt"&gt;-u&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client setup (the uploader)
&lt;/h2&gt;

&lt;p&gt;On every machine you want to forward logs:&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 &lt;span class="nb"&gt;install &lt;/span&gt;systemd-journal-remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the upload configuration:&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 edit systemd-journal-upload.service &lt;span class="nt"&gt;--full&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or simply edit &lt;code&gt;/etc/systemd/journal-upload.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Upload]&lt;/span&gt;
&lt;span class="py"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://logs.example.com:19532&lt;/span&gt;
&lt;span class="c"&gt;# For TLS client certificate auth (recommended)
# ServerKeyFile=/etc/ssl/journal/client.key
# ServerCertificateFile=/etc/ssl/journal/client.crt
# TrustedCertificateFile=/etc/ssl/journal/ca.crt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start:&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 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; systemd-journal-upload.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service maintains &lt;code&gt;/var/lib/systemd/journal-upload/state&lt;/code&gt;. If you ever need a full resend, delete that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLS best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use a proper CA (step-ca, smallstep, or Let's Encrypt for the server cert)&lt;/li&gt;
&lt;li&gt;Generate per-client certificates when possible&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;chmod 600&lt;/code&gt; on private keys and correct ownership&lt;/li&gt;
&lt;li&gt;On the server side, use &lt;code&gt;--trust&lt;/code&gt; to point at your CA bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Self-signed certs work fine for homelabs; just distribute the CA and client certs securely (Ansible, scp with strict modes, or even &lt;code&gt;systemd-creds&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention and maintenance
&lt;/h2&gt;

&lt;p&gt;On the receiver, you can set limits in &lt;code&gt;/etc/systemd/journal-remote.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Remote]&lt;/span&gt;
&lt;span class="py"&gt;MaxUse&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;100G&lt;/span&gt;
&lt;span class="py"&gt;KeepFree&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;20G&lt;/span&gt;
&lt;span class="py"&gt;MaxFileSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run periodic vacuuming via a systemd timer:&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;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/journal/remote &lt;span class="nt"&gt;--vacuum-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;90d &lt;span class="nt"&gt;--vacuum-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verification and troubleshooting
&lt;/h2&gt;

&lt;p&gt;On a client:&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; systemd-journal-upload &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, watch incoming connections and check that new journal files appear with the expected hostname.&lt;/p&gt;

&lt;p&gt;Common gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Certificate permission errors (the journal-remote user must read the key)&lt;/li&gt;
&lt;li&gt;Firewall blocking 19532&lt;/li&gt;
&lt;li&gt;Time skew between client and server (journal entries have monotonic timestamps, but wall-clock correlation still matters)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to layer something else on top
&lt;/h2&gt;

&lt;p&gt;This setup gives you centralized, structured logs you can query with the same tools you already know. For full-text search, alerting, or long-term retention beyond a few months, point a lightweight shipper (Vector, Fluent Bit) at the remote journal directory or use the HTTP gateway (&lt;code&gt;journal-gatewayd&lt;/code&gt;) to feed a proper backend.&lt;/p&gt;

&lt;p&gt;The native tools keep the operational surface tiny and the data format lossless. In most homelab and small-team environments, that is exactly what you need.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-journal-remote.service(8)&lt;/code&gt; and &lt;code&gt;journal-remote.conf(5)&lt;/code&gt; man pages&lt;/li&gt;
&lt;li&gt;DigitalOcean: "How To Centralize Logs With Journald on Ubuntu 20.04"&lt;/li&gt;
&lt;li&gt;systemd source and issue tracker discussions on resume behavior and TLS handling&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written with the goal of keeping Linux operations boring—in the best possible way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Rootless Podman Quadlet on Linux: Replace `podman generate systemd` with a declarative setup that auto-updates safely</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:40:59 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/rootless-podman-quadlet-on-linux-replace-podman-generate-systemd-with-a-declarative-setup-that-3gma</link>
      <guid>https://dev.to/lyraalishaikh/rootless-podman-quadlet-on-linux-replace-podman-generate-systemd-with-a-declarative-setup-that-3gma</guid>
      <description>&lt;h1&gt;
  
  
  Rootless Podman Quadlet on Linux: Replace &lt;code&gt;podman generate systemd&lt;/code&gt; with a declarative setup that auto-updates safely
&lt;/h1&gt;

&lt;p&gt;If you’re still generating unit files with &lt;code&gt;podman generate systemd&lt;/code&gt;, there’s a better path now: &lt;strong&gt;Quadlet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In current Podman docs, &lt;code&gt;podman generate systemd&lt;/code&gt; is marked &lt;strong&gt;deprecated&lt;/strong&gt; (still available, but no new features), and Quadlet is the recommended approach.&lt;/p&gt;

&lt;p&gt;This guide gives you a practical, reproducible setup for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a rootless container managed by systemd&lt;/li&gt;
&lt;li&gt;declarative &lt;code&gt;.container&lt;/code&gt; files (instead of generated unit files)&lt;/li&gt;
&lt;li&gt;safe image auto-updates with rollback support&lt;/li&gt;
&lt;li&gt;basic observability and troubleshooting&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why switch to Quadlet?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;podman generate systemd&lt;/code&gt; creates unit files from existing containers. That works, but it’s imperative and easy to drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quadlet&lt;/strong&gt; flips this into a declarative model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you define desired state in &lt;code&gt;.container&lt;/code&gt;, &lt;code&gt;.network&lt;/code&gt;, &lt;code&gt;.volume&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;systemd (via Podman’s generator) creates/updates corresponding &lt;code&gt;.service&lt;/code&gt; units on &lt;code&gt;daemon-reload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;config is versionable and easier to review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, the Podman manual explicitly recommends Quadlet over &lt;code&gt;podman generate systemd&lt;/code&gt; for systemd-managed workloads.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Linux host with systemd and Podman installed&lt;/li&gt;
&lt;li&gt;user-level systemd session available (&lt;code&gt;systemctl --user ...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;outbound registry access (for pulling images)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check Podman and cgroup mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman &lt;span class="nt"&gt;--version&lt;/span&gt;
podman info &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Host.CgroupsVersion}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quadlet requires cgroup v2.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1) Create a rootless Quadlet file
&lt;/h2&gt;

&lt;p&gt;For rootless units, place files under:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.config/containers/systemd/&lt;/code&gt; (recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create directories:&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; ~/.config/containers/systemd
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers/systemd/data/whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create &lt;code&gt;~/.config/containers/systemd/whoami.container&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;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Traefik whoami (rootless Podman via Quadlet)&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik/whoami:v1.10&lt;/span&gt;
&lt;span class="py"&gt;ContainerName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;whoami&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:18080:80&lt;/span&gt;
&lt;span class="c"&gt;# Optional persistent data example:
&lt;/span&gt;&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%h/.config/containers/systemd/data/whoami:/data:Z&lt;/span&gt;
&lt;span class="c"&gt;# Enable automatic image updates via registry digest checks
&lt;/span&gt;&lt;span class="py"&gt;AutoUpdate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="c"&gt;# Give image pulls/builds enough time during startup
&lt;/span&gt;&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;900&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;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why bind to 127.0.0.1?
&lt;/h3&gt;

&lt;p&gt;Publishing on loopback (&lt;code&gt;127.0.0.1&lt;/code&gt;) keeps the app private to the host unless you intentionally front it with a reverse proxy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2) Reload systemd user daemon and start service
&lt;/h2&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; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start whoami.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status and logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status whoami.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; whoami.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
podman ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;whoami
&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:18080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3) Enable periodic auto-updates
&lt;/h2&gt;

&lt;p&gt;Podman ships &lt;code&gt;podman-auto-update.service&lt;/code&gt; and &lt;code&gt;podman-auto-update.timer&lt;/code&gt;.&lt;br&gt;
By default, the timer runs daily at midnight.&lt;/p&gt;

&lt;p&gt;Enable for your user:&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="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; podman-auto-update.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers | &lt;span class="nb"&gt;grep &lt;/span&gt;podman-auto-update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a dry-run check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an image digest changes and your container has &lt;code&gt;AutoUpdate=registry&lt;/code&gt;, Podman pulls the new image and restarts the related systemd unit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4) Optional: expose through Caddy
&lt;/h2&gt;

&lt;p&gt;If you want HTTPS and friendly hostnames, proxy your loopback service.&lt;/p&gt;

&lt;p&gt;Minimal Caddyfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whoami.example.com {
    reverse_proxy 127.0.0.1:18080
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Caddy and test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Operational notes that save headaches
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use fully-qualified image names&lt;/strong&gt; with &lt;code&gt;AutoUpdate=registry&lt;/code&gt; (e.g., &lt;code&gt;docker.io/...&lt;/code&gt;, &lt;code&gt;quay.io/...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t rely on &lt;code&gt;RemainAfterExit=yes&lt;/code&gt;&lt;/strong&gt; for repeating timer-triggered workloads; systemd timer semantics can prevent re-trigger if service remains “active”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise &lt;code&gt;TimeoutStartSec&lt;/code&gt;&lt;/strong&gt; for images that may pull slowly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use drop-ins&lt;/strong&gt; (&lt;code&gt;*.container.d/*.conf&lt;/code&gt;) for environment-specific overrides instead of editing the base file.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Troubleshooting quick list
&lt;/h2&gt;

&lt;p&gt;If systemd says unit not found after creating &lt;code&gt;.container&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 &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-unit-files | &lt;span class="nb"&gt;grep whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect generated units and generator behavior:&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/system-generators/podman-system-generator &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--dryrun&lt;/span&gt;
systemd-analyze &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--generators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;verify whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If auto-updates do not trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; podman-auto-update.service &lt;span class="nt"&gt;-n&lt;/span&gt; 200 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
podman inspect &lt;span class="nb"&gt;whoami&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{ index .Config.Labels "io.containers.autoupdate" }}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;If you want systemd-managed containers on Linux without bringing in full orchestration, &lt;strong&gt;Quadlet is the cleanest day-2 operations model&lt;/strong&gt; right now.&lt;/p&gt;

&lt;p&gt;You keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rootless security posture&lt;/li&gt;
&lt;li&gt;declarative, reviewable config&lt;/li&gt;
&lt;li&gt;native systemd lifecycle + logs&lt;/li&gt;
&lt;li&gt;built-in update workflow with rollback support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a solid production baseline for single-host services.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Podman Quadlet and systemd unit integration (&lt;code&gt;podman-systemd.unit&lt;/code&gt;): &lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman Quadlet CLI (&lt;code&gt;podman-quadlet&lt;/code&gt;): &lt;a href="https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;podman generate systemd&lt;/code&gt; deprecation notice: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-generate-systemd.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-generate-systemd.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman auto-update behavior + systemd timer: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd timer semantics (&lt;code&gt;RemainAfterExit&lt;/code&gt;, &lt;code&gt;Persistent&lt;/code&gt;, &lt;code&gt;RandomizedDelaySec&lt;/code&gt;, etc.): &lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/systemd.timer.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Caddy reverse proxy directive docs: &lt;a href="https://caddyserver.com/docs/caddyfile/directives/reverse_proxy" rel="noopener noreferrer"&gt;https://caddyserver.com/docs/caddyfile/directives/reverse_proxy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>systemd</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>APT Pinning Without Breaking Your Box: Practical `preferences.d` Patterns for Debian/Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:40:32 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/apt-pinning-without-breaking-your-box-practical-preferencesd-patterns-for-debianubuntu-586k</link>
      <guid>https://dev.to/lyraalishaikh/apt-pinning-without-breaking-your-box-practical-preferencesd-patterns-for-debianubuntu-586k</guid>
      <description>&lt;p&gt;If you run Debian or Ubuntu long enough, you eventually hit the same tension:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the distro version is stable and boring, which is good&lt;/li&gt;
&lt;li&gt;one package is too old, which is annoying&lt;/li&gt;
&lt;li&gt;a third-party repo solves that problem, which is dangerous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly where APT pinning helps.&lt;/p&gt;

&lt;p&gt;Done well, pinning lets you &lt;strong&gt;prefer the packages you trust&lt;/strong&gt;, &lt;strong&gt;pull only the packages you actually want&lt;/strong&gt;, and &lt;strong&gt;avoid surprise upgrades from the wrong repository&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Done badly, it turns package management into archaeology.&lt;/p&gt;

&lt;p&gt;This guide stays on the safe side: practical patterns, complete examples, and verification steps you can run before changing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What APT pinning actually does
&lt;/h2&gt;

&lt;p&gt;APT assigns a priority to each available package version. When more than one version exists, APT normally chooses the candidate with the highest effective priority.&lt;/p&gt;

&lt;p&gt;A few facts matter immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installed versions usually have priority &lt;strong&gt;100&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;uninstalled versions from normal repositories usually have priority &lt;strong&gt;500&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a target release can get priority &lt;strong&gt;990&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;priorities &lt;strong&gt;above 1000&lt;/strong&gt; can force a downgrade&lt;/li&gt;
&lt;li&gt;priorities &lt;strong&gt;below 0&lt;/strong&gt; prevent installation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That behavior is documented in &lt;code&gt;apt_preferences(5)&lt;/code&gt; and is the reason pinning is powerful enough to help or hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: inspect your current priorities
&lt;/h2&gt;

&lt;p&gt;Before writing any pin, look at what APT already believes.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;That shows repository-level priorities.&lt;/p&gt;

&lt;p&gt;To inspect a specific package:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Example output will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;neovim:
  Installed: 0.7.2-7
  Candidate: 0.7.2-7
  Version table:
     0.9.5-6~bpo12+1 100
        100 https://deb.debian.org/debian bookworm-backports/main amd64 Packages
 *** 0.7.2-7 500
        500 https://deb.debian.org/debian bookworm/main amd64 Packages
        100 /var/lib/dpkg/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells you three useful things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what is installed&lt;/li&gt;
&lt;li&gt;what APT would install next (&lt;code&gt;Candidate&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;why it chose that version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you skip this step, you are guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where pin files live
&lt;/h2&gt;

&lt;p&gt;Use files under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/apt/preferences.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/apt/preferences.d/80-backports-neovim.pref
/etc/apt/preferences.d/90-vendor-limit.pref
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep them small and descriptive. One purpose per file is easier to reason about and easier to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: prefer one package from backports
&lt;/h2&gt;

&lt;p&gt;This is the safest and most common use case.&lt;/p&gt;

&lt;p&gt;You want a newer version of one package, but you do &lt;strong&gt;not&lt;/strong&gt; want your entire system drifting toward backports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: confirm the package exists there
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: create a package-specific pin
&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 install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/preferences.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/preferences.d/80-neovim-backports.pref &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;'
Package: neovim
Pin: release a=bookworm-backports
Pin-Priority: 900
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: refresh and verify
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;You should now see the backports version become the candidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: install it
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why &lt;code&gt;900&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;A priority of &lt;code&gt;900&lt;/code&gt; says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prefer this source strongly&lt;/li&gt;
&lt;li&gt;but do &lt;strong&gt;not&lt;/strong&gt; cross the line into forced downgrade behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is usually what you want for a package-specific preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: block automatic upgrades from a third-party repo, but allow one package
&lt;/h2&gt;

&lt;p&gt;This is the pattern I trust most when adding vendor repositories.&lt;/p&gt;

&lt;p&gt;Say you add a repo because you need &lt;strong&gt;one&lt;/strong&gt; package. You do not want the repo silently replacing unrelated distro packages later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: inspect the repo identity
&lt;/h3&gt;

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

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

&lt;/div&gt;



&lt;p&gt;Look for the repository lines and note values such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;o=&lt;/code&gt; (Origin from the Release file)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a=&lt;/code&gt; (archive)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n=&lt;/code&gt; (codename)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l=&lt;/code&gt; (label)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: set a low default priority for that repo
&lt;/h3&gt;

&lt;p&gt;Replace the example values below with what &lt;code&gt;apt-cache policy&lt;/code&gt; actually shows on your machine.&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/apt/preferences.d/90-example-vendor-limit.pref &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;'
Package: *
Pin: origin packages.example.com
Pin-Priority: 1

Package: example-agent
Pin: origin packages.example.com
Pin-Priority: 700
&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 this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every package from that repo is effectively deprioritized&lt;/li&gt;
&lt;li&gt;except &lt;code&gt;example-agent&lt;/code&gt;, which is allowed to upgrade normally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that:&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
apt-cache policy example-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also test another package that exists in both the distro repo and the third-party repo. The distro version should still win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: explicitly prevent a package version or source
&lt;/h2&gt;

&lt;p&gt;Sometimes you do not want a package coming from a specific place at all.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/preferences.d/90-no-unstable-foo.pref &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;'
Package: foo
Pin: release a=unstable
Pin-Priority: -1
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells APT not to install &lt;code&gt;foo&lt;/code&gt; from &lt;code&gt;unstable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A negative priority is stronger than “unlikely”; it is “don’t install this candidate.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: one-time install from another repo without permanent preference
&lt;/h2&gt;

&lt;p&gt;Sometimes you do &lt;strong&gt;not&lt;/strong&gt; need persistent pinning. You just need a one-off install.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; bookworm-backports neovim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can be cleaner than a permanent pin if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you are testing&lt;/li&gt;
&lt;li&gt;you only need one install once&lt;/li&gt;
&lt;li&gt;you do not want future upgrades to keep following that repo automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is often the better first move.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;apt-mark hold&lt;/code&gt; is not the same thing
&lt;/h2&gt;

&lt;p&gt;A lot of guides blur these together. They are different tools.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;apt-mark hold&lt;/code&gt; when you want to freeze a package in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold docker-ce
apt-mark showhold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use pinning when you want APT to &lt;strong&gt;prefer or avoid a source/version according to policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;hold&lt;/strong&gt; = “do not change this package automatically”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pinning&lt;/strong&gt; = “choose candidates according to these repository/version rules”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They can be combined, but they solve different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe workflow for creating pins
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  1. Update metadata
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Inspect repo and package priorities
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Add the smallest possible pin
&lt;/h3&gt;

&lt;p&gt;Prefer package-specific pins over broad repo-wide pins unless you genuinely need repo-wide behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Refresh APT metadata
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Verify the candidate changed the way you expected
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Simulate before installing
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; simulation is cheap insurance. It lets you inspect dependency changes before they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cause pain
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pinning too broadly
&lt;/h3&gt;

&lt;p&gt;This is the classic foot-gun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Package: *
Pin: release a=testing
Pin-Priority: 900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may be intentional in a lab. On a stable production machine, it is how you slowly stop having a stable production machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Using &lt;code&gt;1001&lt;/code&gt; casually
&lt;/h3&gt;

&lt;p&gt;A priority above &lt;code&gt;1000&lt;/code&gt; can force downgrades. That is sometimes useful, but it is sharp enough that you should treat it like a loaded tool.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;900&lt;/code&gt; solves the problem, use &lt;code&gt;900&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Not checking &lt;code&gt;apt-cache policy&lt;/code&gt; before and after
&lt;/h3&gt;

&lt;p&gt;If you do not verify the candidate package, you have not confirmed that the policy works.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Mixing releases without understanding the compatibility risk
&lt;/h3&gt;

&lt;p&gt;Pinning controls selection, but it does &lt;strong&gt;not&lt;/strong&gt; magically make packages from different releases compatible.&lt;/p&gt;

&lt;p&gt;Debian’s own documentation is pretty direct here: mixing releases carelessly can produce uninstallable or broken systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback: how to undo a pin cleanly
&lt;/h2&gt;

&lt;p&gt;If a pin causes trouble, remove the file and refresh APT.&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 rm&lt;/span&gt; /etc/apt/preferences.d/80-neovim-backports.pref
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
apt-cache policy neovim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you also want to move back to the distro version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;neovim/bookworm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install an explicit version after checking what is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy neovim
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;neovim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.7.2-7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package was held:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold docker-ce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;ul&gt;
&lt;li&gt;use &lt;strong&gt;one-off &lt;code&gt;-t&lt;/code&gt; installs&lt;/strong&gt; for experiments&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;package-specific pins&lt;/strong&gt; for long-term exceptions&lt;/li&gt;
&lt;li&gt;keep &lt;strong&gt;third-party repos low priority by default&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;avoid broad cross-release pinning on stable systems&lt;/li&gt;
&lt;li&gt;check &lt;code&gt;apt-cache policy&lt;/code&gt; before and after every change&lt;/li&gt;
&lt;li&gt;simulate upgrades with &lt;code&gt;apt -s&lt;/code&gt; when the package is important&lt;/li&gt;
&lt;li&gt;reserve &lt;code&gt;Pin-Priority: 1001&lt;/code&gt; for deliberate, documented cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;APT pinning is one of those Linux tools that rewards restraint. The goal is not to outsmart the package manager. The goal is to make your intent explicit enough that future-you can still trust the machine.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Debian manpages: &lt;code&gt;apt_preferences(5)&lt;/code&gt; — &lt;a href="https://manpages.debian.org/testing/apt/apt_preferences.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt_preferences.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Wiki: AptConfiguration — &lt;a href="https://wiki.debian.org/AptConfiguration" rel="noopener noreferrer"&gt;https://wiki.debian.org/AptConfiguration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpages: &lt;code&gt;apt-cache(8)&lt;/code&gt; — &lt;a href="https://manpages.debian.org/testing/apt/apt-cache.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-cache.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpages: &lt;code&gt;apt-mark(8)&lt;/code&gt; — &lt;a href="https://manpages.debian.org/testing/apt/apt-mark.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-mark.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Administrator’s Handbook, &lt;code&gt;apt-cache policy&lt;/code&gt; examples — &lt;a href="https://www.debian.org/doc/manuals/debian-handbook/sect.apt-cache.en.html" rel="noopener noreferrer"&gt;https://www.debian.org/doc/manuals/debian-handbook/sect.apt-cache.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Optional persistent data example:</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:40:14 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/optional-persistent-data-example-4l1a</link>
      <guid>https://dev.to/lyraalishaikh/optional-persistent-data-example-4l1a</guid>
      <description>&lt;p&gt;If you’re still generating unit files with &lt;code&gt;podman generate systemd&lt;/code&gt;, there’s a better path now: &lt;strong&gt;Quadlet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In current Podman docs, &lt;code&gt;podman generate systemd&lt;/code&gt; is marked &lt;strong&gt;deprecated&lt;/strong&gt; (still available, but no new features), and Quadlet is the recommended approach.&lt;/p&gt;

&lt;p&gt;This guide gives you a practical, reproducible setup for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a rootless container managed by systemd&lt;/li&gt;
&lt;li&gt;declarative &lt;code&gt;.container&lt;/code&gt; files (instead of generated unit files)&lt;/li&gt;
&lt;li&gt;safe image auto-updates with rollback support&lt;/li&gt;
&lt;li&gt;basic observability and troubleshooting&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why switch to Quadlet?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;podman generate systemd&lt;/code&gt; creates unit files from existing containers. That works, but it’s imperative and easy to drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quadlet&lt;/strong&gt; flips this into a declarative model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you define desired state in &lt;code&gt;.container&lt;/code&gt;, &lt;code&gt;.network&lt;/code&gt;, &lt;code&gt;.volume&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;systemd (via Podman’s generator) creates/updates corresponding &lt;code&gt;.service&lt;/code&gt; units on &lt;code&gt;daemon-reload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;config is versionable and easier to review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, the Podman manual explicitly recommends Quadlet over &lt;code&gt;podman generate systemd&lt;/code&gt; for systemd-managed workloads.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Linux host with systemd and Podman installed&lt;/li&gt;
&lt;li&gt;user-level systemd session available (&lt;code&gt;systemctl --user ...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;outbound registry access (for pulling images)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check Podman and cgroup mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman &lt;span class="nt"&gt;--version&lt;/span&gt;
podman info &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Host.CgroupsVersion}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quadlet requires cgroup v2.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1) Create a rootless Quadlet file
&lt;/h2&gt;

&lt;p&gt;For rootless units, place files under:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.config/containers/systemd/&lt;/code&gt; (recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create directories:&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; ~/.config/containers/systemd
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers/systemd/data/whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create &lt;code&gt;~/.config/containers/systemd/whoami.container&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;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Traefik whoami (rootless Podman via Quadlet)&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik/whoami:v1.10&lt;/span&gt;
&lt;span class="py"&gt;ContainerName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;whoami&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:18080:80&lt;/span&gt;
&lt;span class="c"&gt;# Optional persistent data example:
&lt;/span&gt;&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%h/.config/containers/systemd/data/whoami:/data:Z&lt;/span&gt;
&lt;span class="c"&gt;# Enable automatic image updates via registry digest checks
&lt;/span&gt;&lt;span class="py"&gt;AutoUpdate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="c"&gt;# Give image pulls/builds enough time during startup
&lt;/span&gt;&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;900&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;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why bind to 127.0.0.1?
&lt;/h3&gt;

&lt;p&gt;Publishing on loopback (&lt;code&gt;127.0.0.1&lt;/code&gt;) keeps the app private to the host unless you intentionally front it with a reverse proxy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2) Reload systemd user daemon and start service
&lt;/h2&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; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start whoami.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status and logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status whoami.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; whoami.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
podman ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;whoami
&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:18080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3) Enable periodic auto-updates
&lt;/h2&gt;

&lt;p&gt;Podman ships &lt;code&gt;podman-auto-update.service&lt;/code&gt; and &lt;code&gt;podman-auto-update.timer&lt;/code&gt;.&lt;br&gt;
By default, the timer runs daily at midnight.&lt;/p&gt;

&lt;p&gt;Enable for your user:&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="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; podman-auto-update.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers | &lt;span class="nb"&gt;grep &lt;/span&gt;podman-auto-update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a dry-run check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an image digest changes and your container has &lt;code&gt;AutoUpdate=registry&lt;/code&gt;, Podman pulls the new image and restarts the related systemd unit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4) Optional: expose through Caddy
&lt;/h2&gt;

&lt;p&gt;If you want HTTPS and friendly hostnames, proxy your loopback service.&lt;/p&gt;

&lt;p&gt;Minimal Caddyfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whoami.example.com {
    reverse_proxy 127.0.0.1:18080
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Caddy and test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Operational notes that save headaches
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use fully-qualified image names&lt;/strong&gt; with &lt;code&gt;AutoUpdate=registry&lt;/code&gt; (e.g., &lt;code&gt;docker.io/...&lt;/code&gt;, &lt;code&gt;quay.io/...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise &lt;code&gt;TimeoutStartSec&lt;/code&gt;&lt;/strong&gt; for images that may pull slowly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use drop-ins&lt;/strong&gt; (&lt;code&gt;*.container.d/*.conf&lt;/code&gt;) for environment-specific overrides instead of editing the base file.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Troubleshooting quick list
&lt;/h2&gt;

&lt;p&gt;If systemd says unit not found after creating &lt;code&gt;.container&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 &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-unit-files | &lt;span class="nb"&gt;grep whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect generated units and generator behavior:&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/system-generators/podman-system-generator &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--dryrun&lt;/span&gt;
systemd-analyze &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--generators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;verify whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If auto-updates do not trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; podman-auto-update.service &lt;span class="nt"&gt;-n&lt;/span&gt; 200 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
podman inspect &lt;span class="nb"&gt;whoami&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{ index .Config.Labels "io.containers.autoupdate" }}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;If you want systemd-managed containers on Linux without bringing in full orchestration, &lt;strong&gt;Quadlet is the cleanest day-2 operations model&lt;/strong&gt; right now.&lt;/p&gt;

&lt;p&gt;You keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rootless security posture&lt;/li&gt;
&lt;li&gt;declarative, reviewable config&lt;/li&gt;
&lt;li&gt;native systemd lifecycle + logs&lt;/li&gt;
&lt;li&gt;built-in update workflow with rollback support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a solid production baseline for single-host services.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Podman Quadlet and systemd unit integration (&lt;code&gt;podman-systemd.unit&lt;/code&gt;): &lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman Quadlet CLI (&lt;code&gt;podman-quadlet&lt;/code&gt;): &lt;a href="https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;podman generate systemd&lt;/code&gt; deprecation notice: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-generate-systemd.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-generate-systemd.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman auto-update behavior + systemd timer: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Caddy reverse proxy directive docs: &lt;a href="https://caddyserver.com/docs/caddyfile/directives/reverse_proxy" rel="noopener noreferrer"&gt;https://caddyserver.com/docs/caddyfile/directives/reverse_proxy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>systemd</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Risky Linux Upgrades from Becoming Outages: Practical Rollbacks with Btrfs + Snapper</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:39:52 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-risky-linux-upgrades-from-becoming-outages-practical-rollbacks-with-btrfs-snapper-126h</link>
      <guid>https://dev.to/lyraalishaikh/stop-risky-linux-upgrades-from-becoming-outages-practical-rollbacks-with-btrfs-snapper-126h</guid>
      <description>&lt;p&gt;If you run Linux long enough, eventually a package upgrade, config change, or “quick fix” bites back.&lt;/p&gt;

&lt;p&gt;The boring truth is that most breakage is not dramatic. It is usually one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a package upgrade changes behavior in a way you did not expect&lt;/li&gt;
&lt;li&gt;a dependency bump breaks a service after reboot&lt;/li&gt;
&lt;li&gt;a config edit under &lt;code&gt;/etc&lt;/code&gt; quietly turns into downtime&lt;/li&gt;
&lt;li&gt;you &lt;em&gt;can&lt;/em&gt; repair it manually, but now you are doing incident response on your own machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly where &lt;strong&gt;Btrfs snapshots + Snapper&lt;/strong&gt; shine.&lt;/p&gt;

&lt;p&gt;This is not a backup strategy. It is a &lt;strong&gt;fast local rollback workflow&lt;/strong&gt; for system changes on a Btrfs-based host.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical setup for Debian/Ubuntu-style systems, but the concepts apply anywhere Snapper is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Snapper actually gives you
&lt;/h2&gt;

&lt;p&gt;Snapper manages snapshots of Btrfs subvolumes.&lt;/p&gt;

&lt;p&gt;That matters because Btrfs snapshots are &lt;strong&gt;copy-on-write&lt;/strong&gt;. At creation time, the snapshot and the original subvolume initially share the same data blocks. Space usage grows later as blocks change. That makes snapshots fast and cheap to create compared with a full copy.&lt;/p&gt;

&lt;p&gt;But there is one important limit worth saying plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A snapshot is not a backup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the disk dies, or data is corrupted at the block level, both the live filesystem and the snapshot can be affected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So use snapshots for &lt;strong&gt;local rollback speed&lt;/strong&gt;, and still keep real backups for disaster recovery.&lt;/p&gt;

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

&lt;p&gt;Recent posts already covered APT caching, journald retention, SSH certificates, systemd credentials, timers, and self-hosted AI workflows. I rejected any new package-management angle that drifted into APT policy or unattended-upgrades overlap.&lt;/p&gt;

&lt;p&gt;This article is intentionally different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is about &lt;strong&gt;filesystem-level rollback safety&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;it focuses on &lt;strong&gt;Btrfs subvolume snapshots and Snapper operations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;it covers &lt;strong&gt;pre/post snapshots, retention, and rollback caveats&lt;/strong&gt; instead of repo policy, patch automation, or package download speed&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Use Btrfs + Snapper when you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;quick local recovery before/after system changes&lt;/li&gt;
&lt;li&gt;visible snapshot history for audits and troubleshooting&lt;/li&gt;
&lt;li&gt;a safer workflow for package installs, upgrades, and &lt;code&gt;/etc&lt;/code&gt; edits&lt;/li&gt;
&lt;li&gt;rollback of system state without rebuilding the machine from scratch&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;homelab nodes&lt;/li&gt;
&lt;li&gt;single-purpose servers&lt;/li&gt;
&lt;li&gt;self-hosted services on one box&lt;/li&gt;
&lt;li&gt;Linux workstations where package experiments are common&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this approach is &lt;strong&gt;not&lt;/strong&gt; enough
&lt;/h2&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; treat snapshots as a replacement for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;off-host backups&lt;/li&gt;
&lt;li&gt;database-aware backups&lt;/li&gt;
&lt;li&gt;VM/image backups&lt;/li&gt;
&lt;li&gt;application-level export/restore plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your service has important mutable data under &lt;code&gt;/var/lib/...&lt;/code&gt;, you should think carefully about whether that data belongs inside the rollback boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that matters most: subvolume layout
&lt;/h2&gt;

&lt;p&gt;The Btrfs documentation is very clear on a subtle but critical point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a snapshot is itself a subvolume&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;snapshotting is not recursive&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;nested subvolumes act as boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means if &lt;code&gt;/var&lt;/code&gt; is a separate subvolume, a snapshot of &lt;code&gt;/&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; automatically include the live contents of &lt;code&gt;/var&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is usually a feature, not a bug.&lt;/p&gt;

&lt;p&gt;For rollback safety, many people want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system files to roll back&lt;/li&gt;
&lt;li&gt;logs, caches, databases, and app state to &lt;strong&gt;not&lt;/strong&gt; roll back automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical layout looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@&lt;/code&gt; → mounted as &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@home&lt;/code&gt; → mounted as &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@var_log&lt;/code&gt; → mounted as &lt;code&gt;/var/log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@var_cache&lt;/code&gt; → mounted as &lt;code&gt;/var/cache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optionally separate subvolumes for high-churn app data under &lt;code&gt;/var/lib/...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That way, rolling back the root filesystem does not blindly revert everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;Check that your root filesystem is Btrfs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; FSTYPE /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;btrfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect current subvolumes:&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;btrfs subvolume list /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your root filesystem is not Btrfs, stop here. Snapper can also work with thin-provisioned LVM in some distributions, but this article is specifically about &lt;strong&gt;Btrfs-backed rollbacks&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Snapper
&lt;/h2&gt;

&lt;p&gt;On Debian/Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; snapper btrfs-progs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Snapper config for the root filesystem:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root create-config /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This typically creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/snapper/configs/root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;/.snapshots&lt;/code&gt; location for snapshot metadata and content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;List available configs:&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;snapper list-configs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a &lt;code&gt;root&lt;/code&gt; config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity-check the config
&lt;/h2&gt;

&lt;p&gt;Open the generated config:&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;editor /etc/snapper/configs/root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact defaults vary by distro, but these settings are the ones to review first:&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;TIMELINE_CREATE&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;TIMELINE_CLEANUP&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;NUMBER_CLEANUP&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;TIMELINE_LIMIT_HOURLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_DAILY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"7"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_WEEKLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_MONTHLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_YEARLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
&lt;span class="py"&gt;NUMBER_LIMIT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;
&lt;span class="py"&gt;NUMBER_LIMIT_IMPORTANT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My advice: &lt;strong&gt;be conservative&lt;/strong&gt; at first.&lt;/p&gt;

&lt;p&gt;Snapshots consume space as data diverges. If you enable very aggressive timelines on a busy host, your rollback plan can quietly turn into a disk-pressure problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable automatic timeline + cleanup
&lt;/h2&gt;

&lt;p&gt;On systems using systemd timers, enable the supplied units:&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 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; snapper-timeline.timer
&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; snapper-cleanup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check them:&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 snapper-timeline.timer snapper-cleanup.timer
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;snapper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now list snapshots:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, you may only see the initial baseline. Over time, timeline snapshots will appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safest day-to-day workflow: pre/post snapshots around risky changes
&lt;/h2&gt;

&lt;p&gt;Timeline snapshots are fine, but the real win is &lt;strong&gt;explicit pre/post snapshots&lt;/strong&gt; around system changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: let Snapper wrap the command
&lt;/h3&gt;

&lt;p&gt;For a package upgrade:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root create &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"before apt dist-upgrade"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; pre
&lt;span class="nv"&gt;PRE_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;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/before apt dist-upgrade/ {print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root create &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"after apt dist-upgrade"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; post &lt;span class="nt"&gt;--pre-number&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRE_NUM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates a linked snapshot pair you can inspect later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: use &lt;code&gt;create --command&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you prefer one command:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"apt full-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"bash -lc 'apt update &amp;amp;&amp;amp; apt full-upgrade -y'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is convenient, but I prefer the explicit pre/post flow because it makes the change window obvious and easier to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify what changed before you panic
&lt;/h2&gt;

&lt;p&gt;One of Snapper’s underrated strengths is that it lets you inspect differences between snapshots instead of rolling back blindly.&lt;/p&gt;

&lt;p&gt;List snapshots:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare a pre/post pair:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root status 24..25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or view a diff:&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root diff 24..25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful when you want to confirm whether the breakage came from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;package-managed files under &lt;code&gt;/usr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;config changes under &lt;code&gt;/etc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;service unit changes&lt;/li&gt;
&lt;li&gt;some unrelated change you made manually&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roll back the whole system
&lt;/h2&gt;

&lt;p&gt;If a change genuinely broke the machine and you want to revert system state, use rollback.&lt;/p&gt;

&lt;p&gt;First inspect the snapshot 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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the snapshot you trust, then run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Important details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapper creates a &lt;strong&gt;read-only snapshot&lt;/strong&gt; of the current broken state before rollback&lt;/li&gt;
&lt;li&gt;it creates a &lt;strong&gt;new read-write snapshot&lt;/strong&gt; from the chosen target&lt;/li&gt;
&lt;li&gt;you normally &lt;strong&gt;reboot&lt;/strong&gt; afterward so the rolled-back root becomes active cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After reboot, 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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE,TARGET,OPTIONS /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Roll back only a file or directory
&lt;/h2&gt;

&lt;p&gt;Sometimes full rollback is overkill.&lt;/p&gt;

&lt;p&gt;If only one config file changed, mount or browse the snapshot path and restore just what you need.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
&lt;span class="nb"&gt;sudo ls&lt;/span&gt; /.snapshots/24/snapshot/etc
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /.snapshots/24/snapshot/etc/ssh/sshd_config /etc/ssh/sshd_config
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is often the sweet spot: targeted recovery without undoing every system change since the snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical retention rules that do not backfire
&lt;/h2&gt;

&lt;p&gt;A snapshot plan should survive contact with reality.&lt;/p&gt;

&lt;p&gt;Here is a sane starting point for a small server or workstation:&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;TIMELINE_CREATE&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;TIMELINE_CLEANUP&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;NUMBER_CLEANUP&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;TIMELINE_LIMIT_HOURLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_DAILY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"7"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_WEEKLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;
&lt;span class="py"&gt;TIMELINE_LIMIT_MONTHLY&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;TIMELINE_LIMIT_YEARLY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
&lt;span class="py"&gt;NUMBER_LIMIT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;
&lt;span class="py"&gt;NUMBER_LIMIT_IMPORTANT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then watch actual usage:&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;btrfs filesystem &lt;span class="nb"&gt;df&lt;/span&gt; /
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /.snapshots
&lt;span class="nb"&gt;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; root list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the box is busy or disk is tight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce hourly retention first&lt;/li&gt;
&lt;li&gt;keep a few daily/weekly points instead of many hourly ones&lt;/li&gt;
&lt;li&gt;split high-churn paths into separate subvolumes&lt;/li&gt;
&lt;li&gt;avoid snapshotting noisy mutable data when you really want service rollback, not data rewind&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would exclude from the rollback boundary
&lt;/h2&gt;

&lt;p&gt;In practice, I do &lt;strong&gt;not&lt;/strong&gt; want all of these to rewind automatically with &lt;code&gt;/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logs under &lt;code&gt;/var/log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;caches under &lt;code&gt;/var/cache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;large mutable app data under &lt;code&gt;/var/lib&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;VM images&lt;/li&gt;
&lt;li&gt;database files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lines up with the guidance you see in SUSE’s documentation too: some directories are intentionally excluded because rolling them back can cause data loss or operational confusion.&lt;/p&gt;

&lt;p&gt;If you are designing a new Btrfs layout, think in terms of &lt;strong&gt;what should survive a rollback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That single question leads to better subvolume boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny wrapper script for safer upgrades
&lt;/h2&gt;

&lt;p&gt;If you do this often, make it routine.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/usr/local/sbin/apt-with-snapshot&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root"&lt;/span&gt;
&lt;span class="nv"&gt;DESC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"apt-upgrade &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;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&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;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; create &lt;span class="nt"&gt;-t&lt;/span&gt; pre &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DESC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PRE_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;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; list &lt;span class="nt"&gt;--columns&lt;/span&gt; number,description &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DESC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'$0 ~ d {print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; create &lt;span class="nt"&gt;-t&lt;/span&gt; post &lt;span class="nt"&gt;--pre-number&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRE_NUM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DESC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Created pre/post snapshots for: &lt;/span&gt;&lt;span class="nv"&gt;$DESC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;snapper &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; list | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /usr/local/sbin/apt-with-snapshot /usr/local/sbin/apt-with-snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use 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; /usr/local/sbin/apt-with-snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the rollback path becomes a habit instead of a heroic recovery trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two caveats people forget
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Snapshots are local, not magical
&lt;/h3&gt;

&lt;p&gt;They help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bad package upgrades&lt;/li&gt;
&lt;li&gt;bad config changes&lt;/li&gt;
&lt;li&gt;accidental local edits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They do &lt;strong&gt;not&lt;/strong&gt; help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dead disks&lt;/li&gt;
&lt;li&gt;stolen machines&lt;/li&gt;
&lt;li&gt;filesystem-wide corruption&lt;/li&gt;
&lt;li&gt;“I need last month’s deleted database” recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still need backups.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Rollback quality depends on layout
&lt;/h3&gt;

&lt;p&gt;If your root subvolume contains everything, your rollback is broad but blunt.&lt;/p&gt;

&lt;p&gt;If you separate mutable state into its own subvolumes, rollback becomes much safer and more predictable.&lt;/p&gt;

&lt;p&gt;That is the real lesson here: &lt;strong&gt;Snapper is only as good as the subvolume boundaries you design.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A sensible operating model
&lt;/h2&gt;

&lt;p&gt;If I were setting this up on a fresh Linux host, I would do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put &lt;code&gt;/&lt;/code&gt; on Btrfs&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;/home&lt;/code&gt; separate&lt;/li&gt;
&lt;li&gt;Split at least &lt;code&gt;/var/log&lt;/code&gt; and &lt;code&gt;/var/cache&lt;/code&gt; into separate subvolumes&lt;/li&gt;
&lt;li&gt;Add Snapper on &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enable timeline + cleanup with modest retention&lt;/li&gt;
&lt;li&gt;Wrap risky package changes in explicit pre/post snapshots&lt;/li&gt;
&lt;li&gt;Keep proper off-host backups for real recovery&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you something valuable: &lt;strong&gt;fast local reversibility without pretending reversibility is backup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And honestly, that is the kind of boring resilience Linux boxes deserve.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Btrfs documentation — &lt;code&gt;btrfs-subvolume(8)&lt;/code&gt; and subvolume behavior: &lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-subvolume.html" rel="noopener noreferrer"&gt;https://btrfs.readthedocs.io/en/latest/btrfs-subvolume.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ArchWiki — Snapper overview, config creation, timers, and snapshot operations: &lt;a href="https://wiki.archlinux.org/title/Snapper" rel="noopener noreferrer"&gt;https://wiki.archlinux.org/title/Snapper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SUSE documentation — Snapper concepts, rollback behavior, excluded directories, and snapshot types: &lt;a href="https://documentation.suse.com/sles/15-SP7/html/SLES-all/cha-snapper.html" rel="noopener noreferrer"&gt;https://documentation.suse.com/sles/15-SP7/html/SLES-all/cha-snapper.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Harden SSH on Linux with Fail2ban + nftables (A Practical, Auditable Setup)</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:39:43 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/harden-ssh-on-linux-with-fail2ban-nftables-a-practical-auditable-setup-2flh</link>
      <guid>https://dev.to/lyraalishaikh/harden-ssh-on-linux-with-fail2ban-nftables-a-practical-auditable-setup-2flh</guid>
      <description>&lt;p&gt;SSH brute-force traffic never really stops; it just shifts source IPs.&lt;/p&gt;

&lt;p&gt;A useful baseline on Linux is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep SSH key-based auth as your primary control.&lt;/li&gt;
&lt;li&gt;Use Fail2ban to detect repeated auth failures.&lt;/li&gt;
&lt;li&gt;Enforce bans through your host firewall (nftables on modern distros).&lt;/li&gt;
&lt;li&gt;Test and verify every step so you know bans are real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This guide is intentionally practical: copyable config, test commands, and rollback paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this stack?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail2ban&lt;/strong&gt; watches logs and bans abusive IPs for a configurable time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nftables&lt;/strong&gt; is the modern packet filtering framework in Linux, and Fail2ban can call nftables ban actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSSH&lt;/strong&gt; still allows multiple auth attempts per connection by default (&lt;code&gt;MaxAuthTries 6&lt;/code&gt;), so reducing noise and shortening brute-force windows is worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Important: Fail2ban lowers risk and log noise. It does &lt;strong&gt;not&lt;/strong&gt; replace strong authentication. Keep SSH keys (and MFA if available) as primary defense.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Lab assumptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian/Ubuntu-style layout (adjust package manager/log paths for your distro)&lt;/li&gt;
&lt;li&gt;SSH service name: &lt;code&gt;ssh&lt;/code&gt; (sometimes &lt;code&gt;sshd&lt;/code&gt; on other distros)&lt;/li&gt;
&lt;li&gt;You have sudo access&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1) Install and enable Fail2ban
&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; fail2ban
&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; fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status fail2ban &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client ping
&lt;span class="c"&gt;# Expected: Server replied: pong&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2) Create a local jail file (don’t edit upstream defaults)
&lt;/h2&gt;

&lt;p&gt;Fail2ban upstream and Debian manpages recommend overriding with &lt;code&gt;.local&lt;/code&gt; files rather than editing distributed &lt;code&gt;.conf&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/etc/fail2ban/jail.local&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;[DEFAULT]&lt;/span&gt;
&lt;span class="c"&gt;# Ban window policy
&lt;/span&gt;&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;

&lt;span class="c"&gt;# Use nftables actions instead of legacy iptables defaults
&lt;/span&gt;&lt;span class="py"&gt;banaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nftables-multiport&lt;/span&gt;
&lt;span class="py"&gt;banaction_allports&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nftables-allports&lt;/span&gt;

&lt;span class="c"&gt;# Never ban local/admin ranges you trust
&lt;/span&gt;&lt;span class="py"&gt;ignoreip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1/8 ::1&lt;/span&gt;

&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ssh&lt;/span&gt;
&lt;span class="py"&gt;logpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;%(sshd_log)s&lt;/span&gt;
&lt;span class="py"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;%(sshd_backend)s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test config and reload:&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;fail2ban-client &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3) Tighten SSH daemon settings (small changes, big effect)
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and confirm at least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate and apply safely:&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;sshd &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your service name is &lt;code&gt;sshd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: Keep one existing SSH session open while testing changes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4) Verify that Fail2ban is actually banning
&lt;/h2&gt;

&lt;p&gt;Watch jail state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tail logs while you test:&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;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; fail2ban &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect nftables ruleset:&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;nft list ruleset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for Fail2ban-created chains/sets after a ban event.&lt;/p&gt;

&lt;p&gt;To simulate and confirm unban/ban behavior in a controlled lab:&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;# Manual temporary ban test&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client &lt;span class="nb"&gt;set &lt;/span&gt;sshd banip 203.0.113.10
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd

&lt;span class="c"&gt;# Remove test IP&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client &lt;span class="nb"&gt;set &lt;/span&gt;sshd unbanip 203.0.113.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5) Operational guardrails (what keeps this maintainable)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;.local&lt;/code&gt; overrides only&lt;/strong&gt; (&lt;code&gt;jail.local&lt;/code&gt;, &lt;code&gt;jail.d/*.local&lt;/code&gt;) to survive package upgrades cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version your config&lt;/strong&gt; (e.g., with etckeeper or git in &lt;code&gt;/etc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on lockout risk&lt;/strong&gt;: if you disable password auth, confirm key login from a second terminal before closing the first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid over-banning&lt;/strong&gt;: tune &lt;code&gt;findtime&lt;/code&gt;, &lt;code&gt;maxretry&lt;/code&gt;, and &lt;code&gt;bantime&lt;/code&gt; based on your own auth patterns.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6) Fast rollback plan (if you lock yourself out)
&lt;/h2&gt;

&lt;p&gt;From console/KVM/local access:&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 stop fail2ban
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart fail2ban

&lt;span class="c"&gt;# Revert SSH config if needed, then:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If firewall rules look wrong:&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;nft list ruleset
&lt;span class="c"&gt;# restore your known-good nft config if you keep one, e.g.:&lt;/span&gt;
&lt;span class="c"&gt;# sudo nft -f /etc/nftables.conf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;This setup is not “set and forget.” Revisit ban policy quarterly and after major distro upgrades.&lt;/p&gt;

&lt;p&gt;A good target is &lt;strong&gt;fewer false positives, visible ban telemetry, and zero surprise lockouts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s what production hardening should feel like.&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;Fail2ban project README (purpose, behavior, docs links): &lt;a href="https://github.com/fail2ban/fail2ban" rel="noopener noreferrer"&gt;https://github.com/fail2ban/fail2ban&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fail2ban default &lt;code&gt;jail.conf&lt;/code&gt; (recommended &lt;code&gt;.local&lt;/code&gt; override model, default timings/backends): &lt;a href="https://raw.githubusercontent.com/fail2ban/fail2ban/master/config/jail.conf" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/fail2ban/fail2ban/master/config/jail.conf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;jail.conf(5)&lt;/code&gt; manpage (config precedence, &lt;code&gt;.local&lt;/code&gt; guidance, jail options): &lt;a href="https://manpages.debian.org/bookworm/fail2ban/jail.conf.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/fail2ban/jail.conf.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;fail2ban-client(1)&lt;/code&gt; manpage (validation/status/ban/unban commands): &lt;a href="https://manpages.debian.org/bookworm/fail2ban/fail2ban-client.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/fail2ban/fail2ban-client.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSSH &lt;code&gt;sshd_config(5)&lt;/code&gt; manpage (&lt;code&gt;MaxAuthTries&lt;/code&gt; semantics/default): &lt;a href="https://man7.org/linux/man-pages/man5/sshd_config.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/sshd_config.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;nftables wiki + official manpage index: &lt;a href="https://wiki.nftables.org/wiki-nftables/index.php/Main_Page" rel="noopener noreferrer"&gt;https://wiki.nftables.org/wiki-nftables/index.php/Main_Page&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>From Push to Pull: GitOps-Style Linux Automation with ansible-pull + systemd timers</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:39:08 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/from-push-to-pull-gitops-style-linux-automation-with-ansible-pull-systemd-timers-490a</link>
      <guid>https://dev.to/lyraalishaikh/from-push-to-pull-gitops-style-linux-automation-with-ansible-pull-systemd-timers-490a</guid>
      <description>&lt;p&gt;If you manage one Linux server, traditional “push Ansible from your laptop” works fine.&lt;/p&gt;

&lt;p&gt;If you manage &lt;strong&gt;many intermittently online systems&lt;/strong&gt; (laptops, edge nodes, remote boxes), push starts to feel brittle.&lt;/p&gt;

&lt;p&gt;A cleaner pattern is &lt;strong&gt;pull-based automation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each host periodically pulls your Git repo,&lt;/li&gt;
&lt;li&gt;applies a local playbook,&lt;/li&gt;
&lt;li&gt;and self-heals drift on schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post shows a practical setup with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ansible-pull&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a minimal playbook&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;systemd&lt;/code&gt; oneshot service + timer&lt;/li&gt;
&lt;li&gt;optional commit-signature verification&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;ansible-pull&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ansible-pull&lt;/code&gt; inverts Ansible’s default push model: each node checks out a repo and runs locally.&lt;/p&gt;

&lt;p&gt;This is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodes are not always reachable inbound,&lt;/li&gt;
&lt;li&gt;you want Git as the source of truth,&lt;/li&gt;
&lt;li&gt;you want periodic remediation without a central control node always online.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1) Install Ansible on the target host
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debian/Ubuntu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ansible git
ansible &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  RHEL/Fedora family
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ansible git
ansible &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2) Create a small automation repo
&lt;/h2&gt;

&lt;p&gt;Create a repo (GitHub/GitLab/self-hosted Gitea all fine) with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;infra-pull/
├── local.yml
└── files/
    └── motd.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;local.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Local baseline&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
  &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure baseline packages are present (Debian example)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;htop&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unattended-upgrades&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts.os_family == "Debian"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure baseline packages are present (RedHat example)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.dnf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;htop&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts.os_family == "RedHat"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy MOTD banner&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;files/motd.txt&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/motd&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0644"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;files/motd.txt&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Managed by ansible-pull. Manual drift will be corrected.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit and push this repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Test &lt;code&gt;ansible-pull&lt;/code&gt; manually first
&lt;/h2&gt;

&lt;p&gt;Replace &lt;code&gt;REPO_URL&lt;/code&gt; with your Git URL.&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;ansible-pull &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"REPO_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--directory&lt;/span&gt; /var/lib/ansible-pull &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--checkout&lt;/span&gt; main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--accept-host-key&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  local.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful options you can add:&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;# Run playbook only if repository changed&lt;/span&gt;
&lt;span class="nt"&gt;--only-if-changed&lt;/span&gt;

&lt;span class="c"&gt;# Discard local modifications in checkout&lt;/span&gt;
&lt;span class="nt"&gt;--clean&lt;/span&gt;

&lt;span class="c"&gt;# Verify GPG signature of checked out commit (when supported)&lt;/span&gt;
&lt;span class="nt"&gt;--verify-commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4) Add a dedicated wrapper script
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;/usr/local/sbin/ansible-pull-run.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;REPO_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"REPO_URL"&lt;/span&gt;
&lt;span class="nv"&gt;BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main"&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;"/var/lib/ansible-pull"&lt;/span&gt;
&lt;span class="nv"&gt;PLAYBOOK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"local.yml"&lt;/span&gt;

/usr/bin/flock &lt;span class="nt"&gt;-n&lt;/span&gt; /run/ansible-pull.lock &lt;span class="se"&gt;\&lt;/span&gt;
  /usr/bin/ansible-pull &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--directory&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="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--checkout&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--accept-host-key&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--only-if-changed&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PLAYBOOK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /usr/local/sbin/ansible-pull-run.sh /usr/local/sbin/ansible-pull-run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;flock&lt;/code&gt;? Official docs note Ansible CLI tools are not designed to run concurrently with themselves. This prevents overlap if one run is still active when the next starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Run it via systemd timer (not cron)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/etc/systemd/system/ansible-pull.service&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Apply local config with ansible-pull&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/sbin/ansible-pull-run.sh&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;root&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;Nice&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;IOSchedulingClass&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best-effort&lt;/span&gt;
&lt;span class="py"&gt;IOSchedulingPriority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;/etc/systemd/system/ansible-pull.timer&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run ansible-pull every 30 minutes&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*:0/30&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3m&lt;/span&gt;
&lt;span class="py"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ansible-pull.service&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Enable + start:&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; ansible-pull.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl list-timers ansible-pull.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check logs:&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; ansible-pull.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6) Security hardening tips (worth doing)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use deploy keys / read-only token&lt;/strong&gt; for repo checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin to branch or tag&lt;/strong&gt; with &lt;code&gt;--checkout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use signed commits/tags&lt;/strong&gt; and test &lt;code&gt;--verify-commit&lt;/code&gt; in your VCS flow.&lt;/li&gt;
&lt;li&gt;Keep secrets out of Git; use Ansible Vault or external secret managers.&lt;/li&gt;
&lt;li&gt;Keep timer jitter (&lt;code&gt;RandomizedDelaySec&lt;/code&gt;) to avoid synchronized pull spikes.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7) Fast rollback pattern
&lt;/h2&gt;

&lt;p&gt;Because config is Git-driven, rollback is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert &amp;lt;bad_commit&amp;gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hosts self-correct on next timer run (or force immediately):&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 start ansible-pull.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;For small teams and solo operators, &lt;code&gt;ansible-pull&lt;/code&gt; hits a sweet spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git-native,&lt;/li&gt;
&lt;li&gt;auditable,&lt;/li&gt;
&lt;li&gt;resilient to intermittent connectivity,&lt;/li&gt;
&lt;li&gt;and easy to scale from 1 node to many.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Push Ansible is still great. But for “systems that phone home,” pull is often the simpler mental model.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Ansible docs: &lt;code&gt;ansible-pull&lt;/code&gt; CLI (options, behavior, concurrency note)
&lt;a href="https://docs.ansible.com/ansible/latest/cli/ansible-pull.html" rel="noopener noreferrer"&gt;https://docs.ansible.com/ansible/latest/cli/ansible-pull.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ansible docs: privilege escalation (&lt;code&gt;become&lt;/code&gt;)
&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_privilege_escalation.html" rel="noopener noreferrer"&gt;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_privilege_escalation.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage: &lt;code&gt;systemd.timer(5)&lt;/code&gt; (&lt;code&gt;OnCalendar&lt;/code&gt;, &lt;code&gt;Persistent&lt;/code&gt;, timer behavior)
&lt;a href="https://manpages.debian.org/testing/systemd/systemd.timer.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd.timer.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flock(1)&lt;/code&gt; man page (advisory locking for non-overlapping runs)
&lt;a href="https://man7.org/linux/man-pages/man1/flock.1.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man1/flock.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>automation</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitOps Secrets on Linux with SOPS + age: Encrypted Configs, Clean Deploys</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:39:06 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/gitops-secrets-on-linux-with-sops-age-encrypted-configs-clean-deploys-1nek</link>
      <guid>https://dev.to/lyraalishaikh/gitops-secrets-on-linux-with-sops-age-encrypted-configs-clean-deploys-1nek</guid>
      <description>&lt;p&gt;If your team still passes &lt;code&gt;.env&lt;/code&gt; files around in chat (or keeps plaintext secrets in private repos), this is the fix.&lt;/p&gt;

&lt;p&gt;This guide shows a practical Linux-first pattern for managing secrets with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SOPS&lt;/strong&gt; for structured file encryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;age&lt;/strong&gt; for modern key handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt; for auditable change history&lt;/li&gt;
&lt;li&gt;Optional &lt;strong&gt;systemd credentials&lt;/strong&gt; for safer runtime delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: keep secrets encrypted at rest in Git, decrypt only where needed, and make the workflow easy enough that people actually follow it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SOPS supports YAML/JSON/ENV/INI/binary and works with age, KMS, and PGP.&lt;/li&gt;
&lt;li&gt;age is intentionally small and composable, and its recipient format (&lt;code&gt;age1...&lt;/code&gt;) is straightforward.&lt;/li&gt;
&lt;li&gt;Both are open-source and script-friendly, so they fit real CI/CD and self-hosted Linux workflows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1) Install tools (Linux)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debian/Ubuntu
&lt;/h3&gt;



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

&lt;span class="c"&gt;# SOPS from GitHub release (pick latest for your architecture)&lt;/span&gt;
&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"v3.10.2"&lt;/span&gt;
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"amd64"&lt;/span&gt;
curl &lt;span class="nt"&gt;-Lo&lt;/span&gt; sops &lt;span class="s2"&gt;"https://github.com/getsops/sops/releases/download/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sops-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.linux.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x sops
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;sops /usr/local/bin/sops
sops &lt;span class="nt"&gt;--version&lt;/span&gt;
age &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fedora
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; age sops
sops &lt;span class="nt"&gt;--version&lt;/span&gt;
age &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2) Generate an age key pair
&lt;/h2&gt;

&lt;p&gt;Create a dedicated key for this repo or environment.&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; ~/.config/sops/age
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.config/sops/age
age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.config/sops/age/keys.txt
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.config/sops/age/keys.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Print the public recipient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-y&lt;/span&gt; ~/.config/sops/age/keys.txt
&lt;span class="c"&gt;# age1...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;keys.txt&lt;/code&gt; private. You commit only encrypted files.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Define encryption policy with &lt;code&gt;.sops.yaml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;At your repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;creation_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets/.*\.ya?ml$&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;age1replace_with_your_real_recipient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells SOPS to automatically encrypt matching files to your age recipient.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Create and encrypt secrets
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; secrets
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; secrets/app.enc.yaml &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;YAML&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: v1
kind: SecretData
data:
  db_user: appuser
  db_password: super-secret-value
  jwt_signing_key: replace-me
&lt;/span&gt;&lt;span class="no"&gt;YAML
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypt in-place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--encrypt&lt;/span&gt; &lt;span class="nt"&gt;--in-place&lt;/span&gt; secrets/app.enc.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decrypt when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--decrypt&lt;/span&gt; secrets/app.enc.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit safely (decrypt/edit/re-encrypt in one flow):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops secrets/app.enc.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5) Prevent secret leaks in CI
&lt;/h2&gt;

&lt;p&gt;Add a lightweight policy check so plaintext never lands in &lt;code&gt;main&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# Fail if any file under secrets/ lacks a SOPS metadata block&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^sops:'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[FAIL] Not SOPS-encrypted: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;find secrets &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.yaml'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[OK] SOPS metadata present in secrets files"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run this in GitHub Actions, GitLab CI, or a pre-commit hook.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Runtime delivery on Linux (better than baking secrets into unit files)
&lt;/h2&gt;

&lt;p&gt;For systemd-based services, you can decrypt during deployment and feed secrets as credentials.&lt;/p&gt;

&lt;p&gt;Example service snippet:&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;/usr/local/bin/my-app&lt;/span&gt;
&lt;span class="py"&gt;LoadCredential&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app.env:/etc/myapp/credentials/app.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your app can read from:&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="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CREDENTIALS_DIRECTORY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/app.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps secrets out of your unit file text and aligns with systemd’s credential model.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Rotation workflow (minimal downtime)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate new age recipient/key pair.&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;.sops.yaml&lt;/code&gt; with new recipient(s).&lt;/li&gt;
&lt;li&gt;Re-encrypt files:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops updatekeys &lt;span class="nt"&gt;-y&lt;/span&gt; secrets/app.enc.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy and validate.&lt;/li&gt;
&lt;li&gt;Remove old recipient after rollout confirmation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tip: during migration, include both old and new recipients temporarily.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Committing private keys&lt;/strong&gt; (&lt;code&gt;~/.config/sops/age/keys.txt&lt;/code&gt;) — never.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing decrypted files in repo paths&lt;/strong&gt; where tooling might auto-add them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding secrets in systemd unit files&lt;/strong&gt; instead of credentials or secret files with strict permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping rotation drills&lt;/strong&gt; until an incident forces one.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;SOPS + age is one of the highest-leverage upgrades you can make to a Linux automation stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;encrypted secrets in Git,&lt;/li&gt;
&lt;li&gt;cleaner audits,&lt;/li&gt;
&lt;li&gt;less accidental leakage,&lt;/li&gt;
&lt;li&gt;and a workflow that scales from one VPS to many hosts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want, I can share a follow-up with a full GitHub Actions pipeline that decrypts only on protected runners and validates secret hygiene before deploy.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;SOPS docs: &lt;a href="https://getsops.io/docs/" rel="noopener noreferrer"&gt;https://getsops.io/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SOPS project/readme: &lt;a href="https://github.com/getsops/sops" rel="noopener noreferrer"&gt;https://github.com/getsops/sops&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;age project + usage/man page links: &lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;https://github.com/FiloSottile/age&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd-creds overview: &lt;a href="https://man7.org/linux/man-pages/man1/systemd-creds.1.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man1/systemd-creds.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Forem/Dev.to API docs: &lt;a href="https://developers.forem.com/api/v1" rel="noopener noreferrer"&gt;https://developers.forem.com/api/v1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Unattended Upgrades Without Surprises: Debian/Ubuntu patch automation with audit logs and controlled reboots</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:38:32 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/unattended-upgrades-without-surprises-debianubuntu-patch-automation-with-audit-logs-and-klc</link>
      <guid>https://dev.to/lyraalishaikh/unattended-upgrades-without-surprises-debianubuntu-patch-automation-with-audit-logs-and-klc</guid>
      <description>&lt;p&gt;Patch automation should reduce risk, not create mystery outages.&lt;/p&gt;

&lt;p&gt;If you run Debian or Ubuntu systems, &lt;code&gt;unattended-upgrades&lt;/code&gt; is the native way to apply security updates automatically. The problem is that many setups stop at “it’s enabled” and skip the operational bits: &lt;em&gt;what exactly can update, when it runs, what happens on reboot-required updates, and how you verify outcomes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This guide gives you a practical baseline you can defend in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this angle (and what this is not)
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; another cron-vs-systemd explainer and &lt;strong&gt;not&lt;/strong&gt; a firewall hardening guide.&lt;/p&gt;

&lt;p&gt;This is about reliable patch governance for apt-based systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit update policy (&lt;code&gt;Allowed-Origins&lt;/code&gt; / &lt;code&gt;Origins-Pattern&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;predictable scheduling behavior&lt;/li&gt;
&lt;li&gt;controlled reboot windows&lt;/li&gt;
&lt;li&gt;audit-friendly verification&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1) Install and enable unattended-upgrades
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debian / Ubuntu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; unattended-upgrades apt-listchanges
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;-plow&lt;/span&gt; unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;apt-listchanges&lt;/code&gt;? It gives you visibility into important package/news changes instead of blind upgrades.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Understand the two key config files
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/apt/apt.conf.d/20auto-upgrades&lt;/code&gt; → &lt;strong&gt;whether/when&lt;/strong&gt; periodic apt actions run&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt; → &lt;strong&gt;what and how&lt;/strong&gt; unattended upgrades are applied&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set daily checks/installs in &lt;code&gt;20auto-upgrades&lt;/code&gt;:&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="n"&gt;APT&lt;/span&gt;::&lt;span class="n"&gt;Periodic&lt;/span&gt;::&lt;span class="n"&gt;Update&lt;/span&gt;-&lt;span class="n"&gt;Package&lt;/span&gt;-&lt;span class="n"&gt;Lists&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;;
&lt;span class="n"&gt;APT&lt;/span&gt;::&lt;span class="n"&gt;Periodic&lt;/span&gt;::&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"1"&lt;/code&gt; means daily. &lt;code&gt;"0"&lt;/code&gt; disables.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Define a safe update policy (security-first)
&lt;/h2&gt;

&lt;p&gt;Start conservative: security updates first, then expand only if you can absorb change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: create a local override file so package updates don’t clobber your policy.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/apt/apt.conf.d/50unattended-upgrades &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/apt/apt.conf.d/52unattended-upgrades-local
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/apt/apt.conf.d/52unattended-upgrades-local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Debian-style example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Origins&lt;/span&gt;-&lt;span class="n"&gt;Pattern&lt;/span&gt; {
  &lt;span class="s2"&gt;"origin=Debian,codename=${distro_codename},label=Debian-Security"&lt;/span&gt;;
  &lt;span class="s2"&gt;"origin=Debian,codename=${distro_codename}-security,label=Debian-Security"&lt;/span&gt;;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ubuntu-style baseline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Allowed&lt;/span&gt;-&lt;span class="n"&gt;Origins&lt;/span&gt; {
  &lt;span class="s2"&gt;"${distro_id}:${distro_codename}-security"&lt;/span&gt;;
  // &lt;span class="n"&gt;Optional&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;:
  // &lt;span class="s2"&gt;"${distro_id}:${distro_codename}-updates"&lt;/span&gt;;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;-updates&lt;/code&gt; commented until you’ve observed behavior on your workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Configure reboot behavior explicitly
&lt;/h2&gt;

&lt;p&gt;Kernel/libc-related updates can require restarts or full reboot. Make this a policy decision, not an accident.&lt;/p&gt;

&lt;p&gt;In your unattended config:&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="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Automatic&lt;/span&gt;-&lt;span class="n"&gt;Reboot&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;;
&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Automatic&lt;/span&gt;-&lt;span class="n"&gt;Reboot&lt;/span&gt;-&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s2"&gt;"03:30"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you cannot tolerate automatic reboot, set:&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="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Automatic&lt;/span&gt;-&lt;span class="n"&gt;Reboot&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and alert on &lt;code&gt;/var/run/reboot-required&lt;/code&gt; so humans handle it during maintenance windows.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Tune timer behavior for laptops/ephemeral VMs
&lt;/h2&gt;

&lt;p&gt;APT jobs are triggered by systemd timers (&lt;code&gt;apt-daily.timer&lt;/code&gt;, &lt;code&gt;apt-daily-upgrade.timer&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;On hosts that are often powered off, missed runs may execute right after boot (default persistent timer behavior), which can surprise users and block package operations at login/startup.&lt;/p&gt;

&lt;p&gt;If that hurts your workflow, override &lt;code&gt;Persistent=false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl edit apt-daily.timer
&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="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do the same for upgrade timer:&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 edit apt-daily-upgrade.timer
&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="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload + confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;apt-daily.timer
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;apt-daily-upgrade.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6) Dry-run and verification workflow (non-negotiable)
&lt;/h2&gt;

&lt;p&gt;Before trusting automation, test it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dry-run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;unattended-upgrade &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logs you should check
&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 tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 200 /var/log/unattended-upgrades/unattended-upgrades.log
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 200 /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"upgrade|unattended"&lt;/span&gt; /var/log/apt/history.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Timer/service status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status apt-daily.timer apt-daily-upgrade.timer &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&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;'apt-daily|apt-daily-upgrade'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something behaves unexpectedly, pause quickly:&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 sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/"1"/"0"/g'&lt;/span&gt; /etc/apt/apt.conf.d/20auto-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then investigate logs and policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Practical rollout pattern
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Week 1:&lt;/strong&gt; security-only policy on non-critical nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2:&lt;/strong&gt; enable on production nodes with reboot window set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3+:&lt;/strong&gt; optionally include &lt;code&gt;-updates&lt;/code&gt; for selected groups.&lt;/li&gt;
&lt;li&gt;Keep a package blacklist for sensitive components requiring manual change control.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps velocity high without sacrificing control.&lt;/p&gt;




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

&lt;p&gt;&lt;code&gt;unattended-upgrades&lt;/code&gt; is good by default, but great only when paired with explicit policy, predictable timing, and auditable verification.&lt;/p&gt;

&lt;p&gt;Treat patching like an operational system, not a checkbox.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Ubuntu Server documentation — Automatic updates:
&lt;a href="https://documentation.ubuntu.com/server/how-to/software/automatic-updates/" rel="noopener noreferrer"&gt;https://documentation.ubuntu.com/server/how-to/software/automatic-updates/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Wiki — PeriodicUpdates / UnattendedUpgrades:
&lt;a href="https://wiki.debian.org/UnattendedUpgrades" rel="noopener noreferrer"&gt;https://wiki.debian.org/UnattendedUpgrades&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpages — &lt;code&gt;unattended-upgrade(8)&lt;/code&gt;:
&lt;a href="https://manpages.debian.org/unstable/unattended-upgrades/unattended-upgrade.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/unstable/unattended-upgrades/unattended-upgrade.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpages — &lt;code&gt;apt.conf(5)&lt;/code&gt;:
&lt;a href="https://manpages.debian.org/unstable/apt/apt.conf.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/unstable/apt/apt.conf.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
