Set It and Forget It: Automating Linux Patches with unattended-upgrades

Security updates while you sleep

The most common way servers get compromised is not some dazzling zero-day wielded by a state actor. It is a known vulnerability, with a published fix that has been sitting in the distribution’s repositories for weeks, on a box where nobody ran the update. Patching is unglamorous, easy to defer, and quietly catastrophic when neglected. The fix is to take human procrastination out of the loop entirely — and on Debian and Ubuntu, the tool for that is unattended-upgrades.

When a vulnerability is disclosed and a patch ships, a clock starts. Attackers read the same advisories you do, and automated scanning tools begin probing the internet for unpatched hosts within hours, sometimes minutes. The window between “fix available” and “fix applied” is exactly the window in which you are exposed to a problem that is already solved.

This is the uncomfortable truth our piece on the Psychology of Patch Management circles: the failure is almost never technical, it is behavioural. We intend to patch, we mean to get to it, and then something more urgent always comes along. Automation wins because it does not get distracted, does not deprioritise, and does not go on holiday.

If automatic patching is so obviously good, why doesn’t everyone do it? Because updates occasionally break things. A new package version changes a default, a kernel update upsets a driver, a library bump nudges an application into misbehaviour. Operators who have been burned by a bad update learn to be cautious, and that caution hardens into “we’ll patch manually, carefully” — which in practice becomes “we’ll patch eventually, maybe”.

The resolution is nuance rather than abstinence. Security updates are low-risk and high-value: they tend to be minimal, targeted changes, and the cost of not applying them is severe. Feature updates carry more risk and less urgency. A sensible default is to automate security updates aggressively while keeping a human in the loop for larger version jumps. That is precisely the line unattended-upgrades lets you draw.

The package is usually a command or two away:

sudo apt update
sudo apt install unattended-upgrades apt-listchanges

apt-listchanges summarises what changed in each upgrade, which is handy in the notification emails. Now enable the automated runs:

sudo dpkg-reconfigure --priority=low unattended-upgrades

This writes /etc/apt/apt.conf.d/20auto-upgrades, which controls the schedule cadence:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

The 1 values mean “do this daily” (the unit is days). With these in place, a systemd timer — apt-daily.timer and apt-daily-upgrade.timer — handles the actual runs, with a randomised delay so the whole fleet doesn’t hammer the mirrors at once.

The behaviour you really care about lives in /etc/apt/apt.conf.d/50unattended-upgrades. This is the single most important file in the whole setup, and it rewards a careful read. Open it and look at the Allowed-Origins (Ubuntu) or Origins-Pattern (Debian) block. To restrict automatic upgrades to security only, keep the security line enabled and comment out the rest. On Ubuntu:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
//  "${distro_id}:${distro_codename}-updates";
//  "${distro_id}:${distro_codename}-backports";
};

On Debian the equivalent stanza references Debian-Security. With only the security origin active, you get the urgent fixes automatically while leaving general updates for a human to review and apply on their own schedule.

While you are in this file, you can also tell it never to touch certain packages — useful for a database or anything you upgrade by hand:

Unattended-Upgrade::Package-Blacklist {
    "postgresql";
    "linux-image-";
};

A few other knobs in the same file are worth knowing. Unattended-Upgrade::Remove-Unused-Dependencies "true"; cleans up orphaned packages so disk usage doesn’t creep, and Unattended-Upgrade::MinimalSteps "true"; breaks the upgrade into smaller transactions so that, if the run is interrupted, you are left in a recoverable state rather than half-way through a single giant operation. Both are sensible defaults for an unattended context, where nobody is watching to clean up after a partial failure.

Some updates, the kernel especially, only take full effect after a reboot. unattended-upgrades can reboot for you, but this is the setting that demands the most thought. In the same 50unattended-upgrades file:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

This reboots only when needed, at 04:00, and -WithUsers "false" holds off if someone is logged in. For a single host an automatic reboot in a quiet window is often the right call — an unpatched kernel waiting indefinitely for a manual reboot is a half-applied fix. For a cluster, never let every node reboot at the same moment; either stagger the reboot times or, better, leave Automatic-Reboot "false" and orchestrate reboots through your own rolling process so you never lose quorum. Consider needrestart or live-patching where a reboot is genuinely unacceptable.

You want to know what happened, especially when something fails. Configure an address:

Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";

on-change emails you whenever packages are upgraded or an error occurs, rather than every single day — signal over noise. This requires a working local mail setup (a relay such as msmtp or a configured MTA).

For after-the-fact inspection, the logs live in /var/log/unattended-upgrades/, and the systemd journal records the timer runs:

sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
journalctl -u apt-daily-upgrade.service --since "yesterday"

If you are on a Red Hat-family distribution, the same job is done by dnf-automatic:

sudo dnf install dnf-automatic

Edit /etc/dnf/automatic.conf to choose what it does. The key settings are upgrade_type = security to restrict to security updates, apply_updates = yes to actually install rather than merely download, and an [emitters] section for notifications. Then enable the timer:

sudo systemctl enable --now dnf-automatic-install.timer

The philosophy is identical: automate the urgent, low-risk security fixes and keep humans for the rest.

Do not enable automation blindly and walk away. Run a dry run to see what would be upgraded:

sudo unattended-upgrades --dry-run --debug

This prints the candidate packages and the origins it considers, so you can confirm your Allowed-Origins filtering does what you expect. Watch the first few real cycles, read the notification emails, and check the logs. Only once you have seen it behave sensibly should you let it run truly unattended.

Automation is a tool, not a religion, and a few caveats keep it from biting you. Stage updates where you can — let a non-critical or pre-production host take the patches first, then promote to production once you are confident. Pair the automation with monitoring (see our piece on Uptime Kuma) so that if a patch does break a service, you find out in minutes rather than from a customer. For fleets, drive patching through configuration management or a rolling orchestrator so reboots are staggered and capacity is preserved. And always keep a tested backup, because the safety net that lets you patch boldly is the ability to roll back without panic.

Unpatched, known vulnerabilities are the bread and butter of opportunistic attackers, and the only reliable defence is to remove the human procrastination from the loop. unattended-upgrades on Debian and Ubuntu — and dnf-automatic on Fedora and RHEL — lets you apply security fixes automatically, restrict to the low-risk updates that matter most, control reboot windows, and get notified when anything changes. Configure it thoughtfully, test it before you trust it, back it with monitoring and backups, and let your servers patch themselves while you sleep.