Set Up Automatic Security Updates on Ubuntu and Debian

unattended-upgrades on Ubuntu and Debian: enable it, pick origins, decide on auto-reboot, test with --dry-run, read the logs. Plus the gotchas that bite.

Share
Set Up Automatic Security Updates on Ubuntu and Debian

Patching by hand works until you have more than three servers. Then it becomes "I'll do it Friday" forever. unattended-upgrades is the boring, correct answer: security updates go in daily, without you, and you only get involved when a reboot is needed.

TL;DR:

sudo apt install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades   # answer Yes
sudo unattended-upgrade --dry-run --debug

That's enough to get security updates installed daily on both Ubuntu and Debian. The rest of this post is about the two decisions you still have to make — what gets updated and whether the box reboots itself — and how to check it's actually running.

Is it already on?

On Ubuntu Server it usually is. On Debian it depends on the install method — the netinst with "standard system utilities" doesn't always give it to you, many cloud images do. Check:

apt-config dump APT::Periodic::Update-Package-Lists APT::Periodic::Unattended-Upgrade
systemctl list-timers 'apt-daily*'

You want both values at "1" and two timers: apt-daily.timer (refreshes package lists) and apt-daily-upgrade.timer (runs the actual upgrade, early morning with a random delay). If the values are missing or "0", the timers run but do nothing.

The two files

/etc/apt/apt.conf.d/20auto-upgrades — the on/off switch

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

"1" means every day. AutocleanInterval clears out old downloaded .debs weekly so /var/cache/apt doesn't quietly grow. dpkg-reconfigure writes the first two lines; I add the third.

/etc/apt/apt.conf.d/50unattended-upgrades — what and how

This one ships with the package and is heavily commented. Don't rewrite it. Override in your own file so package updates don't fight you over it:

sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-local

Files are read in order and later ones win for scalar values, so 52- overrides 50-.

What gets updated: origins

Ubuntu — the default 50unattended-upgrades already has the right list:

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
};

The plain ${distro_codename} line is there for packages whose security fix needs a dependency from the release pocket. -updates is commented out by default. I leave it that way on servers — security fixes only, bugfix updates I take manually when I'm looking.

Debian uses Origins-Pattern instead:

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

The first line is point-release updates — on Debian stable those are conservative and I keep them on. The -security line is the one that matters on Bookworm and newer.

To see which origins your system actually has, look at what apt knows:

apt-cache policy | grep -E 'o=|origin' | sort -u

Gotcha: third-party repos are not covered

Docker, Temurin, nginx.org, PostgreSQL's PGDG — anything you added yourself is not in the allowed list. Those packages never update automatically. That's usually what you want for Docker (an upgrade restarts dockerd, which stops your containers unless you've configured live-restore), but it means you need a separate habit for them. A monthly apt list --upgradable is the minimum.

If you do want one in, find its origin/label with apt-cache policy and add it explicitly. Don't blanket-allow everything.

Blacklisting packages

Some things I never want touched without me watching — usually the database:

Unattended-Upgrade::Package-Blacklist {
    "mariadb-";
    "postgresql-";
};

These are regexes matched against package names, so "mariadb-" catches the whole family. Security fixes for those get applied when I do it, with a backup taken first.

The reboot decision

Library updates (openssl, glibc) need service restarts. Kernel updates need a reboot. unattended-upgrades does neither by default — it installs, then drops /var/run/reboot-required.

Two options, both valid:

Auto-reboot — for boxes where a two-minute outage at 4am doesn't matter:

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

WithUsers "true" reboots even if someone is logged in. If it's false and you leave a tmux session attached, the box never reboots and you won't notice.

Manual reboot with alerting — for anything stateful, game servers, anything where players notice. Leave auto-reboot off and check:

cat /var/run/reboot-required.pkgs 2>/dev/null

I'd rather have a check that pings me than a box running a six-week-old kernel because "it'll reboot eventually". Pipe that into whatever alerting you already have — a systemd service that posts to Discord works fine for this.

Gotcha: auto-reboot on a game server

If the box runs a game server under systemd, an automatic reboot is a hard stop for every player online. Most game servers save on SIGTERM, but not all of them save cleanly, and none of them warn players first. For those boxes: auto-reboot off, reboot manually in a maintenance window after an in-game warning.

Cleanup options

Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "false";

Old kernels are the classic way /boot fills up and the next kernel install fails halfway. Keep Remove-Unused-Kernel-Packages on. Remove-Unused-Dependencies (the broad one) I leave off — it can remove things you installed as a dependency and later started using directly. If /boot or / fills anyway: disk-full forensics.

Mail reports

Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";

Only useful if the box can actually send mail. Most of mine can't, so I read the logs instead. "only-on-error" is the other sane value.

Test it — don't wait for tomorrow

sudo unattended-upgrade --dry-run --debug 2>&1 | less

Look for Allowed origins are: near the top (your list, after variable expansion) and Packages that will be upgraded: near the bottom. If the package list is empty but apt list --upgradable shows security updates, your origins are wrong.

To force a real run right now:

sudo unattended-upgrade -v

Where to look afterwards

less /var/log/unattended-upgrades/unattended-upgrades.log
less /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
grep -h ' upgrade ' /var/log/dpkg.log | tail -20
journalctl -u apt-daily-upgrade.service --since "3 days ago"

The first log tells you what it decided, the second has the dpkg output when something failed. These files are rotated by logrotate, so don't expect history from three months back.

Gotcha: needrestart prompts

On Ubuntu 22.04+ needrestart runs after upgrades and checks which services still use old libraries. Interactively it shows a menu; unattended it's supposed to behave, but I've had manual apt upgrade sessions sit at that dialog over a flaky SSH connection. To make it just restart services automatically:

echo "\$nrconf{restart} = 'a';" | sudo tee /etc/needrestart/conf.d/50-autorestart.conf

Trade-off: it will restart things like ssh and your web server on its own. Usually fine. Not fine for a service that has an expensive restart — those you add to its exclude list.

Gotcha: the dpkg lock at boot

apt-daily.timer fires shortly after boot on a fresh VM. Your provisioning script runs apt install at the same moment and dies with Could not get lock /var/lib/dpkg/lock-frontend. Either wait for it:

while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 5; done

or add -o DPkg::Lock::Timeout=300 to your apt calls. Newer apt supports that and it's cleaner than a loop.

Where this fits

This is step 7 in my Ubuntu initial setup checklist, and it's baked into my Proxmox cloud-init template so new VMs start patched. It doesn't replace thinking about security — SSH hardening and a firewall still matter — but it removes the most common reason servers get owned: a known bug with a fix that was available for months.


Related posts