Unattended Upgrades: Automating Security Updates
AI generated
$
/etc
Unattended Upgrades · APT · Patch Management · Linux
Unattended Upgrades
Automating security updates without losing control

Unpatched servers are the most common entry point for attackers, but fully automatic updates without oversight carry their own risk. This article shows how unattended-upgrades reliably applies security patches, reports failures by email, and moves reboots into a plannable maintenance window instead of interrupting operations unpredictably.

14 min read unattended-upgrades · apt-listchanges · systemd timers Debian 12 · Ubuntu 22.04/24.04

1. The tradeoff: staying patched vs. keeping control

Most successful server compromises do not exploit zero-day vulnerabilities, they exploit long-known weaknesses for which a patch has been available for weeks or months. If updates are only applied manually, patching almost inevitably gets postponed because other tasks take priority. Unattended Upgrades solve exactly that problem: security patches are downloaded and installed automatically, without an administrator having to maintain every single server by hand.

The price of that automation is a real risk: a faulty update can destabilize a service, overwrite a configuration file, or trigger an unexpected restart tied to a kernel package. The answer is not to abandon automation, but to scope it deliberately: patch security repositories automatically, report failures by email immediately, and move restarts into a plannable maintenance window. That way the server stays current without an update at three in the morning quietly taking down a production service.

This article covers configuring unattended-upgrades on Debian and Ubuntu based systems, from the base installation through restricting it to security-relevant packages, all the way to a clean separation between automatic patching and controlled reboots.

2. Installation and base configuration

The unattended-upgrades package is usually preinstalled on Ubuntu servers, while on Debian it needs to be installed explicitly. After installation, dpkg-reconfigure enables automatic updates in interactive mode and writes the base configuration to /etc/apt/apt.conf.d/20auto-upgrades. This file controls two fundamental switches: whether apt-get update runs automatically every day, and whether unattended-upgrade then installs packages.

The actual logic lives in /etc/apt/apt.conf.d/50unattended-upgrades. This file ships heavily commented, but the defaults are too permissive for production servers: without adjustment, the package may also install updates from the regular -updates repository, not just from -security. Once you understand the file thoroughly, you can tailor it precisely to your own risk appetite instead of relying on distribution defaults.


#!/usr/bin/env bash
# Install and enable unattended-upgrades on a fresh Debian/Ubuntu server
set -euo pipefail

apt-get update
apt-get install -y unattended-upgrades apt-listchanges

# Enable the daily systemd timer / cron entry
dpkg-reconfigure -plow unattended-upgrades

# Verify the base toggle file was written correctly
cat /etc/apt/apt.conf.d/20auto-upgrades
# APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1";

# Confirm the service and timer are active
systemctl status unattended-upgrades.service --no-pager
systemctl list-timers apt-daily-upgrade.timer

One detail that is frequently missed: apt-listchanges shows a changelog summary for affected packages before every upgrade. Combined with unattended-upgrades, that summary automatically ends up in the failure email described in section 5, which substantially improves traceability. Without this package, the log only shows package names and version numbers, with no indication of which specific security issue was actually fixed.

3. Security patches only: setting the origins pattern correctly

The core of risk limitation lives in the Unattended-Upgrade::Origins-Pattern block inside 50unattended-upgrades. By default, this list includes more than just security updates on some distributions. For a conservative approach that only ever applies security-relevant patches automatically, the list must be trimmed down to the -security suite. On Debian systems the relevant origin pattern is origin=Debian,codename=${distro_codename}-security, on Ubuntu it is origin=Ubuntu,archive=${distro_codename}-security.

It is important to comment out every other line referencing -updates, -proposed, or -backports if only security patches should run automatically. Feature updates from the regular -updates channel can bring new functional versions or behavioral changes that deserve deliberate, manual testing before they land on production systems. This separation is the single most important configuration step for resolving the tradeoff described in section 1.


// /etc/apt/apt.conf.d/50unattended-upgrades
// Restrict automatic installs to security-only origins

Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename}-security";
    "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
    // Regular point releases stay disabled, test manually first
    // "origin=Debian,codename=${distro_codename}-updates";
    // "origin=Debian,codename=${distro_codename}-proposed-updates";
};

// Remove unused dependencies after a successful upgrade run
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";

// Keep at most 2 old kernel versions on disk
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";

4. Scheduling: from the cron file to a systemd timer

On current Debian and Ubuntu releases, unattended-upgrades is no longer driven by a classic crontab entry, but by the systemd timer apt-daily-upgrade.timer, which in turn triggers the apt-daily-upgrade.service. By default this timer is set to a randomized point in time each day using RandomizedDelaySec, so that not every server in a fleet hits the same package mirror at once. For a single server that is harmless, in larger environments with a shared mirror the randomization prevents load spikes.

Anyone who wants to control the timing more precisely, for example to force patch runs outside core business hours, should override the timer with a drop-in file instead of editing the system file directly. The correct pattern is systemctl edit apt-daily-upgrade.timer, which automatically creates an override file under /etc/systemd/system/apt-daily-upgrade.timer.d/ that survives the next package update untouched.


# /etc/systemd/system/apt-daily-upgrade.timer.d/override.conf
# Created via: systemctl edit apt-daily-upgrade.timer
# Pin the upgrade run to a fixed early-morning slot instead of a random one

[Timer]
OnCalendar=
OnCalendar=*-*-* 04:15:00
RandomizedDelaySec=300
Persistent=true

5. Configuring email notifications on failure

Automation without feedback is blind trust. The Unattended-Upgrade::Mail block inside 50unattended-upgrades sends no email at all by default, unless an address is configured. With Unattended-Upgrade::MailOnlyOnError "true", delivery is restricted to failed runs, instead of receiving a daily success message that gets ignored after a few days anyway. This combination hits the sweet spot: silence means success, an email means immediate action is required.

A working mail transport on the server is a prerequisite. A minimal msmtp or postfix relay is enough for /usr/sbin/sendmail to actually deliver. A common mistake in practice: the configuration looks correct, but the server has no working mail delivery at all, and failures vanish unnoticed. A test run with a deliberately blocked package reliably surfaces this before you rely on the notification for anything.


// /etc/apt/apt.conf.d/50unattended-upgrades
// Send mail only when something actually goes wrong

Unattended-Upgrade::Mail "ops-alerts@mironsoft.de";
Unattended-Upgrade::MailOnlyOnError "true";
Unattended-Upgrade::Sender "root@$(hostname -f)";

// Verbosity of the report attached to failure mails
Unattended-Upgrade::Verbose "true";
Unattended-Upgrade::Debug "false";

#!/usr/bin/env bash
# Force a dry-run failure to verify the mail pipeline actually delivers
set -euo pipefail

# Simulate an upgrade run without installing anything
unattended-upgrade --dry-run --debug 2>&1 | tee /tmp/uu-dryrun.log

# Send a manual test mail through the same transport used by cron/systemd
echo "Test: unattended-upgrades mail delivery" | \
  mail -s "[TEST] unattended-upgrades on $(hostname -f)" ops-alerts@mironsoft.de

# Check the mail queue for delivery problems
mailq

6. Excluding packages and pinning versions

Some packages should never be updated automatically, even by a change classified as security-relevant, for example the database engine or the PHP interpreter, whenever a production application depends on major-version compatibility. Unattended-Upgrade::Package-Blacklist accepts regex patterns and excludes matching packages from automatic updates entirely, regardless of origin. That is deliberately blunt: once a package is blacklisted, it needs manual maintenance from then on, or open security gaps quietly accumulate.

A finer-grained alternative is APT pinning via /etc/apt/preferences.d/, which sets a maximum version boundary instead of a full exclusion. That lets patch-level updates within a major version continue to apply automatically, while a version jump that might carry breaking changes stays blocked. This combination, a blacklist for critical core components and pinning for everything else, is the most robust approach in production environments.


// /etc/apt/apt.conf.d/50unattended-upgrades
// Never auto-update the database engine or the active PHP runtime

Unattended-Upgrade::Package-Blacklist {
    "mysql-server-*";
    "mariadb-server-*";
    "php8.3-*";
};

# /etc/apt/preferences.d/pin-nginx.pref
# Allow patch-level updates, block the next major version jump

Package: nginx nginx-common nginx-core
Pin: version 1.24.*
Pin-Priority: 1001

7. Putting reboots inside a controlled maintenance window

Kernel updates, glibc patches, and some OpenSSL updates require a restart before the new version actually becomes active. The worst case is a server that runs for weeks with an installed but inactive security patch because nobody noticed the pending reboot. The second-worst case is a server that restarts automatically at an unpredictable time and interrupts active user sessions or running batch jobs in the process. The answer is to automate the reboot, but bind it strictly to a fixed maintenance window.

The options Unattended-Upgrade::Automatic-Reboot and Unattended-Upgrade::Automatic-Reboot-Time control exactly that: a reboot only runs when /var/run/reboot-required exists, and only at the configured time, not immediately after the update. It is also worth adding an independent systemd timer that checks separately from the update run whether a reboot is pending, and executes it in a controlled way with a warning period for logged-in users. That keeps the level of automation high without sacrificing predictability for operations.


// /etc/apt/apt.conf.d/50unattended-upgrades
// Reboot only when actually required, and only inside the maintenance window

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

#!/usr/bin/env bash
# maintenance-reboot.sh: independent check, run inside a fixed window
set -euo pipefail

if [[ ! -f /var/run/reboot-required ]]; then
  echo "[OK] No reboot pending, nothing to do"
  exit 0
fi

echo "[INFO] Reboot required, packages:"
cat /var/run/reboot-required.pkgs 2>/dev/null || true

# Warn logged-in users 5 minutes ahead, then reboot
wall "Maintenance reboot for security updates in 5 minutes."
sleep 300
systemctl reboot

8. Logging, dry runs, and ongoing oversight

Every run of unattended-upgrades writes a detailed log to /var/log/unattended-upgrades/unattended-upgrades.log plus a compact summary of installed packages to unattended-upgrades-dpkg.log. These two files are the first stop for troubleshooting whenever a failure email has arrived or a service shows unexpected behavior after an overnight update. Since both logs grow over time, a logrotate configuration with compression and a sensible retention period belongs on the checklist from day one, not after /var/log has already filled up once.

Before any change to the configuration, it is worth running a dry run with unattended-upgrade --dry-run --debug, which shows exactly which packages would actually be updated based on the current origins pattern and blacklist, without installing anything. This command is also the right tool for verifying, after adjusting the security-only filtering from section 3, that only the intended packages are actually captured before the next automatic run starts.


# /etc/logrotate.d/unattended-upgrades
# Keep six months of compressed history for audit and incident review

/var/log/unattended-upgrades/*.log {
    monthly
    rotate 6
    compress
    delaycompress
    missingok
    notifempty
    create 0640 root adm
}

9. Levels of automation compared

Between fully manual patching and fully automatic updates with no restrictions whatsoever lies a broad spectrum of configuration options. The overview below compares the common levels of automation and shows which approach is advisable for production servers.

Approach Risk Recommendation Reasoning
Purely manual patching Patches stay pending for weeks Not recommended Known gaps stay open because time is scarce
All repositories, fully automatic Feature updates without testing Not recommended Breaking changes land in production unplanned
Security-only, no mail Failed updates go unnoticed Conditionally suitable No feedback channel when something breaks
Security-only + mail on failure Low Recommended Automatically patched, failures visible immediately
+ fixed reboot maintenance window Very low Best practice Patches active, restarts plannable instead of surprising

In practice, the combination of security-only filtering, failure-based email notification, and a fixed reboot window is the approach that best balances automation and control. Anyone who consistently combines these three building blocks minimizes both the risk of unpatched gaps and the risk of uncontrolled outages caused by faulty updates.

Mironsoft

Server hardening, patch management, and Linux operations for Magento infrastructure

Patch servers reliably, without surprises?

We set up unattended-upgrades production-ready: security-only filtering, failure notification by email, and a fixed maintenance window for reboots, tailored to your Magento and infrastructure landscape.

Patch audit

Review the existing update configuration and identify gaps

Automation

Configure security-only updates, blacklist, and mail alerts for production

Maintenance window

Integrate controlled reboot windows into existing deployment processes

10. Summary

Unattended Upgrades resolve the tradeoff between consistently patched servers and the fear of uncontrolled side effects through deliberate scoping rather than giving up automation. Restricting Unattended-Upgrade::Origins-Pattern to -security origins prevents untested feature updates from installing automatically. MailOnlyOnError ensures that silence means success and every email signals an immediate need for action. A package blacklist protects critical core components like the database and the runtime environment from automatic intervention.

Reboots strictly belong inside a fixed maintenance window, driven by Automatic-Reboot-Time or an independent systemd timer with a warning period for users. That keeps the server continuously patched without a restart happening unpredictably in the middle of the day. Regular dry runs with --dry-run --debug and a clean logrotate configuration round out the setup and make every update run traceable.

Automating Unattended Upgrades: The key points at a glance

Security-only patching

Restrict Origins-Pattern to -security, comment out -updates and -backports.

Report failures by mail

MailOnlyOnError "true" plus a working mail transport, verified with a test run.

Exclude critical packages

Package-Blacklist for the database and runtime environment, APT pinning for everything else.

Reboots in a maintenance window

Set Automatic-Reboot-Time to a fixed, quiet window and warn logged-in users first.

11. FAQ: Automating Unattended Upgrades

1What is unattended-upgrades and what is it used for?
A Debian/Ubuntu package that automatically scans defined APT repositories for updates and installs matching packages without manual intervention, mainly for prompt security patching.
2How do I make sure only security updates are installed automatically?
Restrict the origins pattern to the -security suite, comment out every line referencing -updates, -proposed, or -backports.
3How am I notified if an update fails?
Via Unattended-Upgrade::Mail with a target address and MailOnlyOnError set to true, so only real failures trigger an email.
4How do I exclude individual packages from automatic updates?
Via the Package-Blacklist with regex patterns, for example for the database or PHP. APT pinning suits blocking only specific version jumps.
5How do I prevent an unexpected reboot in the middle of the day?
Set Automatic-Reboot-Time to a fixed, low-traffic window, e.g. 04:30. A reboot only runs when one is actually pending.
6How do I know whether a server needs a reboot?
By the existence of /var/run/reboot-required. reboot-required.pkgs lists the triggering packages, usually the kernel or libc6.
7Does unattended-upgrades still run via cron or via systemd?
Via the systemd timer apt-daily-upgrade.timer. Timing is adjustable via systemctl edit apt-daily-upgrade.timer using a drop-in file.
8How do I test the configuration without installing real updates?
With unattended-upgrade --dry-run --debug. Shows exactly which packages would be installed without changing anything.
9Where do I find the logs of completed update runs?
In /var/log/unattended-upgrades/unattended-upgrades.log and unattended-upgrades-dpkg.log, managed through logrotate.
10Is unattended-upgrades suitable for production Magento servers?
Yes, with a blacklist for the database and PHP version plus a fixed reboot maintenance window, keeping the operating system patched without unplanned risk to the application.