Security patches on their own: unattended-upgrades on Ubuntu

Patching is the boring part of server hardening, and the part most often skipped. unattended-upgrades installs only the security updates from the distribution, on a schedule, and writes a log. It can't fix a misconfigured service, but it closes the known holes while you're busy.

Install it

bash
sudo apt update
sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades

Limit it to security updates

The defaults already cover the Ubuntu security pocket. Check the origins in /etc/apt/apt.conf.d/50unattended-upgrades, and set reboots and cleanup explicitly so nothing changes by surprise.

conf
// /etc/apt/apt.conf.d/50unattended-upgrades (relevant lines)
Unattended-Upgrade::Origins-Pattern {
    "origin=Ubuntu,codename=${distro_codename}-security,label=Ubuntu";
};
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Automatic-Reboot "false";

Turn on the daily run

conf
// /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Dry run and status

A dry run shows what would be installed without installing it. Run it once after setup.

bash
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 20
sudo systemctl status unattended-upgrades --no-pager

Check the log and the reboot flag with Python

When a kernel or a core library is updated, Ubuntu writes a reboot flag. This script reports when the log was last written and whether a reboot is pending. It exits with 1 when a reboot is needed, so cron or monitoring can act on it.

python
import datetime as dt
from pathlib import Path

LOG = Path("/var/log/unattended-upgrades/unattended-upgrades.log")
REBOOT = Path("/var/run/reboot-required")


def main() -> int:
    if LOG.exists():
        age = dt.datetime.now() - dt.datetime.fromtimestamp(LOG.stat().st_mtime)
        print(f"unattended-upgrades log last written {age.days} day(s) ago")
    else:
        print("no unattended-upgrades log yet")

    if REBOOT.exists():
        pkgs = REBOOT.with_suffix(".pkgs")
        names = pkgs.read_text().split() if pkgs.exists() else []
        print("reboot required for:", ", ".join(names) or "see /var/run/reboot-required")
        return 1
    print("no reboot required")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Pick a reboot window

A kernel update does nothing until the server restarts. Choose a time with low traffic, reboot, then run the status check and the site checks again. Keeping the reboot manual avoids surprise downtime, and the script above tells you when it's due.

Don't rely on it alone

Automatic patches cover the operating system and its libraries. Your application's dependencies, containers and language runtimes need their own update path. Check them on a schedule, along with the sign-in and firewall settings in the other articles in this series.

Certificates and uptime break quietly, too. osec Monitor watches both for you, free.

Flow: apt update, security only, install, kernel cleanup, reboot flag, your window
Daily security patches; reboot on your schedule.

Sources

• Ubuntu server docs, automatic updates: https://ubuntu.com/server/docs/install/unattended-upgrades
• Debian wiki, unattended-upgrades: https://wiki.debian.org/UnattendedUpgrades