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.
Table of Contents
- 1. The tradeoff: staying patched vs. keeping control
- 2. Installation and base configuration
- 3. Security patches only: setting the origins pattern correctly
- 4. Scheduling: from the cron file to a systemd timer
- 5. Configuring email notifications on failure
- 6. Excluding packages and pinning versions
- 7. Putting reboots inside a controlled maintenance window
- 8. Logging, dry runs, and ongoing oversight
- 9. Levels of automation compared
- 10. Summary
- 11. FAQ
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.