secure-os.org
All guidesQubes OSTailsWhonixHardened LinuxDisk encryptionThreat model
ubuntu

Unattended Upgrades on Ubuntu: What the Default Config Installs, and What It Leaves Off

secure-os· Updated October 7, 2026· 9 min read #ubuntu#updates#hardening#linux#apt
Two hands in dark blue sleeves sliding a green circuit board with a row of network ports into an orange and white equipment chassis

“Automatic security updates are enabled” is a sentence that ends many server audits. It is true on most Ubuntu machines, and it describes less than it seems to. The package that does the work, unattended-upgrades, ships with a configuration file whose comments spell out precisely what is on, what is off, and what was never in scope.

This guide reads that file as shipped by the upstream project, option by option, and shows how to check each point on your own machine.

The short answer

With the stock configuration, unattended-upgrades:

  • installs packages from the release pocket and the -security pocket, plus the ESM security pockets when they are available on the system;
  • does not install from -updates, -proposed or -backports, which are commented out;
  • does not reboot, because Automatic-Reboot defaults to false;
  • does not email anyone, because Unattended-Upgrade::Mail defaults to an empty string;
  • does not touch third-party repositories, because only the listed origins are allowed.

Each of those is a deliberate default, and each one can be changed in about three lines.

Two files, two jobs

The behaviour is split across two files in /etc/apt/apt.conf.d/.

20auto-upgrades is the switch. As shipped upstream it contains exactly two lines:

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

The project README describes their meaning plainly: the system “will check for updates every day, and install them (if that is possible)”. Set either value to "0" and the corresponding step stops.

50unattended-upgrades is the policy: which packages are eligible, and what happens after they are installed. Everything below comes from that second file.

To see the values your machine actually applies, after every fragment has been merged, the README points to one command:

apt-config dump | grep -E 'Periodic|Unattended'

What “allowed origins” really allows

The core of the policy is one list. In the Ubuntu variant of the file it reads:

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";
//	"${distro_id}:${distro_codename}-updates";
//	"${distro_id}:${distro_codename}-proposed";
//	"${distro_id}:${distro_codename}-backports";
};

(The two legacy-security ESM lines are omitted here for length.) Lines starting with // are comments, so -updates is not an allowed origin. A fix that reaches your release only through the -updates pocket waits for a manual apt upgrade.

The comment above the list explains why the plain release pocket is there at all: “in Ubuntu security updates may pull in new dependencies from non-security sources (e.g. chromium). By allowing the release pocket these get automatically pulled in.”

The README adds a rule that matters more than it looks: unattended-upgrades “will not install packages that require dependencies that can’t be fetched from allowed origins”. A security update whose dependency lives in a pocket you have not allowed is held back, without an error on screen.

Third-party repositories are outside the list

Docker’s repository, a PPA, a vendor’s apt source: none of them match ${distro_id}:${distro_codename}-security, so none of them are upgraded automatically. Software installed from those sources stays at the version you last installed by hand.

The README documents the fix. Each repository has an origin and an archive, visible in the o= and a= fields of:

apt-cache policy

Add the pair you find there to your own override file (see below) rather than guessing it. The README also documents Origins-Pattern, which matches on glob-style patterns such as "site=www.example.com,component=main".

A data center aisle with a polished concrete floor, tall black mesh-door racks on both sides showing blue and red cables, and a rolling cart carrying a monitor at the far end.

The reboot nobody scheduled

This is the gap with the largest consequences. The file says:

// Automatically reboot *WITHOUT CONFIRMATION* if
//  the file /var/run/reboot-required is found after the upgrade
//Unattended-Upgrade::Automatic-Reboot "false";

The default is false. When an upgrade needs a restart to take effect, the marker file /var/run/reboot-required is created and nothing else happens. A patched kernel package sits installed while the old kernel keeps running: the patch is on disk, not in memory. The audit sentence “security updates are automatic” stays true the whole time.

Check any machine in one line:

test -f /var/run/reboot-required && echo "reboot pending"

If you want the machine to restart itself, the file offers two settings that belong together:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

Without the second one the default is "now", meaning immediately after the upgrade. Note also that Automatic-Reboot-WithUsers defaults to true: logged-in users do not block the restart unless you set it to false.

On a machine that cannot reboot unattended (a database primary, a host with an encrypted root volume that needs a passphrase at boot), leave the reboot off and solve the other half of the problem instead: make the pending restart visible.

Silence is the default

Unattended-Upgrade::Mail is empty as shipped, and the file is explicit about the consequence: “If empty or unset then no email is sent”. It also states the prerequisite: a working mail setup, with a package that provides mailx.

MailReport accepts three values: "always", "only-on-error" and "on-change". On a single server, "on-change" gives you a record of every package that moved. Across a fleet, "only-on-error" keeps the inbox readable.

If mail is not an option, there is a second channel. SyslogEnable is false by default; set it to true and events go to syslog, which the README notes “is useful in environments where syslog messages are sent to a central store”.

Where to look when nothing seems to happen

The README names the location: “All operations are logged in /var/log/unattended-upgrades/. This includes the dpkg output as well.” The main file is /var/log/unattended-upgrades/unattended-upgrades.log.

To learn what the next run would do without changing anything, use the command the project recommends for debugging:

sudo unattended-upgrade --debug --dry-run

The output lists the allowed origins as the tool understood them, then each candidate package and the reason it was kept or skipped. It is the fastest way to confirm that a repository you added is really matched.

Three causes of a package that did not upgrade are written into the project’s own documentation:

  1. The origin is not allowed. See the list above.
  2. A dependency comes from a non-allowed origin. The package is held back.
  3. The package would prompt about a configuration file. The README states that the tool “will check for conffile prompts before the install and holds back any package that requires them”.

When it runs

The schedule does not live in the unattended-upgrades files. It comes from two systemd timers shipped by apt. In the apt source tree, read on the day this article was written, apt-daily.timer fires at 6,18:00 with RandomizedDelaySec=12h and handles downloads, and apt-daily-upgrade.timer fires at 6:00 with RandomizedDelaySec=60m and handles installation.

Both carry Persistent=true. The systemd documentation defines that setting: the service “is triggered immediately if it would have been triggered at least once during the time when the timer was inactive”, which it describes as useful “to catch up on missed runs of the service when the system was powered down”. A laptop that slept through its window therefore upgrades shortly after it wakes.

Your distribution may ship different values. Read your own:

systemctl cat apt-daily-upgrade.timer
systemctl list-timers 'apt-daily*'

Why shutdown sometimes waits

If a shutdown hangs for a moment while an upgrade is in progress, that is designed behaviour. The MinimalSteps comment explains that the upgrade is split “into the smallest possible chunks so that they can be interrupted with SIGTERM”, and that this makes shutdown during an upgrade possible “with a small delay”. The file also notes that unattended-upgrades raises logind’s InhibitDelayMaxSec to 30 seconds for this purpose.

Cutting power at that moment is the worst response. If it does happen, AutoFixInterruptedDpkg, true by default, runs dpkg --force-confold --configure -a on the next run to finish the interrupted work.

Change it without editing the shipped file

The README is direct on this point: overriding should be done in “a separate APT configuration file fragment”, because changes to the shipped file “may conflict with the local changes blocking updating unattended-upgrades itself”. The new file must sort after 50unattended-upgrades, and the README suggests the name 52unattended-upgrades-local.

A reasonable starting point for a single server, built only from documented options:

// /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Mail "you@example.com";
Unattended-Upgrade::MailReport "on-change";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";

One trap applies to lists. The README warns that to replace a list such as Origins-Pattern you “must first clear the stock-provided list before inserting your own”, with a #clear directive. Otherwise your entries are allowed in addition to the stock ones, not instead of them.

To keep a package out of automatic upgrades, Package-Blacklist takes Python regular expressions. The file’s own example shows the classic mistake: without a trailing $, "libc6" matches every package whose name starts with those characters. The README adds a side effect worth knowing: if package A depends on blacklisted package B, neither A nor B is upgraded.

Turning it off, and why that is rarely the right call

The supported way is the one the README gives:

sudo dpkg-reconfigure -plow unattended-upgrades

It sets the two periodic values for you. There are legitimate reasons to do it: a change-controlled production system where every package movement is scheduled, or a host managed by configuration tooling that applies updates itself.

On any other machine, disabling the mechanism trades a small, bounded risk (an update that misbehaves) for an unbounded one (a known vulnerability that stays open until someone remembers). The narrower tools are usually enough: blacklist the one package that worries you, or restrict runs to chosen days with Update-Days, which accepts values such as {"Mon";"Fri"}.

A five-minute audit

Run these on a server you believe is “updating itself”:

  1. apt-config dump | grep -E 'Periodic|Unattended': are both periodic values "1", and which origins are allowed?
  2. apt-cache policy: which repositories exist on this machine that the allowed list does not cover?
  3. test -f /var/run/reboot-required && echo pending: is a restart waiting?
  4. tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log: when did the last run happen and what did it do?
  5. sudo unattended-upgrade --debug --dry-run: what would the next run do, and what would it hold back?

If the answers are “yes, none, no, last night, nothing held back”, the audit sentence is finally accurate.

The option names, defaults and quoted comments come from the 50unattended-upgrades.Ubuntu and 20auto-upgrades files and the README of the upstream unattended-upgrades repository. The timer values come from the apt source tree, and the definition of Persistent= from the systemd.timer manual. All were read on 7 October 2026. Packaged versions differ between releases, so the file on your own machine is the final authority.

Automatic patching is one layer among several: the wider picture is in the Linux hardening guide, and the service-level controls are covered in systemd service hardening.