Linux hosts

Unattended security upgrades, the secure way

The patch for the bug in the news came out nine days ago. Your host still runs the old version, because patching is on the list for "next sprint", and next sprint has been next sprint since spring.

The short answer

On Debian and Ubuntu, install unattended-upgrades, enable it in 20auto-upgrades, and limit it to the security origins with #clear in a local file. On RHEL and Rocky, set upgrade_type = security and apply_updates = yes in dnf-automatic and enable its timer. Plan reboots, and check the dry run output.

Updated Houssam Hammoudi, CTOTested with unattended-upgrades 2.9.1 (Debian 12), dnf-automatic 4.14.0 (Rocky Linux 9)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

Most attacks on Linux hosts do not use new bugs. They use old ones with a public fix that nobody installed. The window between "fix released" and "fix installed" is where the damage happens.

Manual patching fails for a simple reason: it depends on someone remembering. Automatic security updates remove that dependency for the updates that matter most.

Two traps make automatic updates look on when they are not. On Debian, the package can be installed but disabled, and a local config file that "sets" the allowed origins only adds to the stock list, because apt merges lists. On both families, a kernel or libc update does nothing until the host reboots.

What the docs say

Most versions of Debian install unattended-upgrades with fairly conservative settings, but your system might not have installed the package at all, or might have installed it but disabled it altogether.

Source: Debian wiki, UnattendedUpgrades

The #clear command is the only way to delete a list or a complete scope.

Source: apt.conf(5), Debian 12

What kind of upgrades to look at. default signals looking for all available updates, security only those with an issued security advisory.

Source: DNF documentation, dnf-automatic, upgrade_type

The Debian wiki tells you to copy 50unattended-upgrades to a local file and edit the origins. It does not warn that the list in your file is merged with the stock list, not used instead of it. Without #clear, the stock label=Debian origin stays allowed.

The secure configuration

Debian and Ubuntu:

bash
apt-get install -y unattended-upgrades apt-listchanges
conf
// /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
conf
// /etc/apt/apt.conf.d/52unattended-upgrades-local
// Security updates only. Lists in apt.conf files are MERGED, so first clear the
// list that the stock 50unattended-upgrades built (it also allows "label=Debian").
#clear Unattended-Upgrade::Origins-Pattern;
Unattended-Upgrade::Origins-Pattern {
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};
// Never let a half-configured dpkg block the next run.
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
// Install in small steps, so a shutdown mid-run leaves a consistent system.
Unattended-Upgrade::MinimalSteps "true";
// Remove dependencies that upgrades made unused (old kernels included).
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Reboot when a kernel or libc update needs it, at a quiet hour.
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";
// Tell someone. Needs a working local MTA.
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";

On Ubuntu, the security origin is written "${distro_id}:${distro_codename}-security"; in Allowed-Origins; the #clear rule is the same.

If automatic reboots are not acceptable, set Automatic-Reboot "false" and alert on the file /var/run/reboot-required instead.

RHEL, Rocky, AlmaLinux and Fedora:

bash
dnf install -y dnf-automatic
ini
# /etc/dnf/automatic.conf (the [commands] section)
[commands]
# Only updates that have a security advisory.
upgrade_type = security
download_updates = yes
# Install them, do not just download.
apply_updates = yes
# Reboot only when the kernel, systemd or similar changed.
reboot = when-needed
bash
systemctl enable --now dnf-automatic.timer
systemctl list-timers dnf-automatic.timer

Prove it

The stock Debian list allows label=Debian (all stable updates, not only security):

bash
apt-config dump Unattended-Upgrade::Origins-Pattern
text
Unattended-Upgrade::Origins-Pattern "";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian-Security";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";

The trap: a local file with only the security origin, and no #clear. The stock entries are still there:

text
Unattended-Upgrade::Origins-Pattern "";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian-Security";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";

With the full local file above (#clear first), the list holds only the security origins, and the schedule is on:

bash
apt-config dump APT::Periodic
apt-config dump Unattended-Upgrade::Origins-Pattern
text
APT::Periodic "";
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
Unattended-Upgrade::Origins-Pattern "";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian-Security";
Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";

A dry run shows what the next run would install:

bash
unattended-upgrade --dry-run --debug
text
Starting unattended upgrades script
Allowed origins are: origin=Debian,codename=bookworm,label=Debian-Security, origin=Debian,codename=bookworm-security,label=Debian-Security
Initial blacklist: 
Initial whitelist (not strict): 
Checking: tzdata ([<Origin component:'main' archive:'oldstable-security' origin:'Debian' label:'Debian-Security' site:'deb.debian.org' isTrusted:True>])
pkgs that look like they should be upgraded: tzdata
Option --dry-run given, *not* performing real actions
Packages that will be upgraded: tzdata

Rocky Linux 9, after editing automatic.conf:

bash
grep -E "^(upgrade_type|download_updates|apply_updates|reboot|emit_via)" /etc/dnf/automatic.conf
dnf -q updateinfo list --security | head -5
dnf -q updateinfo list --security | wc -l
text
upgrade_type = security
download_updates = yes
apply_updates = yes
reboot = when-needed
reboot_command = "shutdown -r +5 'Rebooting after applying package updates'"
emit_via = stdio
RLSA-2026:60226 Moderate/Sec.  attr-2.6.0-1.el9_8.x86_64
RLSA-2025:0925  Moderate/Sec.  bzip2-libs-1.0.8-10.el9_5.x86_64
RLSA-2026:66403 Moderate/Sec.  coreutils-single-8.32-41.el9_8.1.x86_64
RLSA-2026:28911 Moderate/Sec.  coreutils-single-8.32-41.el9_8.x86_64
RLSA-2026:1350  Moderate/Sec.  curl-7.76.1-35.el9_7.3.x86_64
123

The container image is a few months old, so it has 123 advisory lines waiting; those are exactly what upgrade_type = security would install. The test script is secure-tests/unattended-security-upgrades/run.sh.

Mistakes people make

Installed but never enabled

unattended-upgrades does nothing without APT::Periodic::Unattended-Upgrade "1". Check with apt-config dump APT::Periodic, and look for a recent run in /var/log/unattended-upgrades/unattended-upgrades.log.

Editing the origins without #clear

Your list is added to the stock list. apt-config dump shows the merged result; trust it, not the file you edited.

Patching without rebooting

A new kernel or libc on disk does not protect a process that still runs the old one. Enable automatic reboots in a window, or alert on /var/run/reboot-required (Debian) and dnf needs-restarting -r (RHEL family).

Nobody reads the result

A failed run looks exactly like a quiet night. Send the report somewhere a person reads it, or alert when the last successful run is older than two days.

Third-party repositories in the allowed list

Adding a vendor origin to the automatic list lets that vendor push code to your hosts at 3 a.m. Keep vendor repositories out, or pin them first.

Checklist

  • unattended-upgrades or dnf-automatic is installed.
  • apt-config dump APT::Periodic shows Unattended-Upgrade "1", or dnf-automatic.timer is enabled.
  • apt-config dump Unattended-Upgrade::Origins-Pattern shows only the origins you chose.
  • upgrade_type = security and apply_updates = yes are set on RHEL-family hosts.
  • A dry run lists the expected security packages.
  • Reboots happen automatically in a window, or an alert fires when one is needed.
  • Run reports reach a person or an alerting system.
  • The last successful run is less than two days old.

Automatic patching will occasionally annoy you. Not patching will eventually embarrass you. Pick the annoyance.

FND

Learn it on a live range

Linux 3: securing Linux, in Foundation: a real host in your browser, and every objective checked on the machine.

Start free

The Secure Way

More on linux hosts

SSH, sudo, firewalls, mount options, updates and the host basics every engineer should get right.

All linux hosts guides