Automatic Security Updates on a VPS: Setting It Up Safely
You log into your VPS to deploy something and see "37 packages can be updated, 12 are security updates." You mean to run apt upgrade later, life happens, and three weeks pass. That gap is exactly how boxes get popped through a known, already-patched CVE. Unattended-upgrades closes that gap automatically - if you set it up right. Get it wrong and it'll silently break your PHP-FPM version or reboot your production server at 2 PM on a Tuesday.
Symptom: You're Either Unpatched or Getting Surprise Reboots
This usually shows up as one of two opposite problems:
- Nothing is patched.
apt list --upgradableshows a pile of security fixes going back months. A vulnerability scanner (or your host's abuse team) flags an outdated OpenSSL, sudo, or Exim package. - Something patched itself and broke. A site goes down after a "System restart required" banner turns into an actual reboot nobody scheduled, or a package update silently restarted a service mid-transaction.
Both come from the same root cause: nobody configured automatic updates deliberately, so the server is either doing nothing or doing everything with no guardrails.
Cause: Fresh VPS Images Don't Patch Themselves
A stock Ubuntu or Debian cloud image ships with the unattended-upgrades package already installed but usually not enabled - the config exists in /etc/apt/apt.conf.d/ but the trigger that actually runs it is off. On some minimal images the package isn't even installed. Either way, security patches just sit in apt's cache until a human runs apt upgrade.
The fix isn't "turn on every update" - that's how you end up with an unplanned MySQL major-version bump. It's turning on security updates specifically, keeping everything else manual, and controlling exactly when (or if) the box reboots.
Fix: Set Up Unattended-Upgrades Properly
1. Install the package
sudo apt update
sudo apt install unattended-upgrades apt-listchanges
2. Enable the daily trigger
Edit /etc/apt/apt.conf.d/20auto-upgrades (create it if it doesn't exist):
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
This tells apt's daily cron/systemd timer to refresh package lists and run unattended-upgrades once a day.
3. Scope it to security updates only
Open /etc/apt/apt.conf.d/50unattended-upgrades and check the Unattended-Upgrade::Allowed-Origins block. On Ubuntu 22.04/24.04 it looks like this by default:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Leave the general -updates line commented out unless you specifically want non-security bug fixes applied automatically too - most production servers shouldn't.
4. Blacklist anything you manage by hand
In the same file, uncomment and fill in the package exclusion list so nothing important moves without you:
Unattended-Upgrade::Package-Blacklist {
"mysql-server-*";
"php8.*";
"docker-ce";
};
This is the step people skip and then discover their WordPress site is down because PHP jumped a minor version and a compiled extension no longer loads.
5. Turn off automatic reboots, or schedule them deliberately
A kernel or glibc security patch will often need a reboot to fully apply. By default some installs leave this off; make it explicit either way:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Setting a fixed low-traffic time beats letting the reboot happen whenever the timer fires. If you'd rather handle reboots manually, set Automatic-Reboot "false" and instead alert on the flag file:
test -f /var/run/reboot-required && echo "Reboot needed on $(hostname)"
Drop that in a daily cron job that emails you, or feed it into whatever monitoring you already use.
6. Get email notifications
Unattended-Upgrade::Mail "you@yourdomain.com";
Unattended-Upgrade::MailReport "on-change";
Make sure the server can actually send outbound mail first - if you've hit Port 25 being blocked on your VPS before, fix that separately or these emails will just vanish.
A quick way to sanity-check this after a patch window: comparedpkg -l | grep -E "openssl|sudo|libssl" output against the CVE advisory that prompted you to look in the first place, so you know the fix actually landed rather than just assuming the cron job ran.
Test It Before Trusting It
Don't wait for the 3 AM cron run to find out your config has a syntax error. Run a dry pass with verbose logging:
sudo unattended-upgrades --dry-run --debug
This shows exactly which packages it would touch and why, without installing anything. Check the tail of /var/log/unattended-upgrades/unattended-upgrades.log afterward - it lists every decision the tool made.
Quick Reference: Key Settings
| Setting | File | What It Controls |
|---|---|---|
| APT::Periodic::Unattended-Upgrade | 20auto-upgrades | Turns the daily run on/off |
| Allowed-Origins | 50unattended-upgrades | Which repos it's allowed to pull from (security vs. all updates) |
| Package-Blacklist | 50unattended-upgrades | Packages it must never touch automatically |
| Automatic-Reboot | 50unattended-upgrades | Whether a needed reboot happens on its own |
| Automatic-Reboot-Time | 50unattended-upgrades | What time that reboot happens |
| Mail / MailReport | 50unattended-upgrades | Who gets notified and how often |
Prevention: Treat This as Infrastructure, Not a Set-and-Forget
A few habits keep automatic patching from becoming its own incident:
- Snapshot before major version jumps. Unattended-upgrades sticking to security patches is safe; a manual
apt full-upgradeacross a distro release is not - take a VPS snapshot first regardless of which one you're running. - Watch the log, don't just trust the email.
/var/log/unattended-upgrades/unattended-upgrades.logis the source of truth if a service acts strange after a patch window. - Keep the blacklist current. Every time you manually pin a service version (a specific PHP build, a MySQL version for compatibility), add it to
Package-Blacklistimmediately - don't rely on memory. - Pair it with monitoring, not instead of it. Automatic patching reduces exposure time; it doesn't replace knowing when a service actually goes down. If you don't already have basic uptime/resource alerts on the box, set those up alongside this.
Once this is running, the ticket that used to read "server got compromised through a three-month-old CVE" mostly stops happening - and you stop being the one who has to remember to run apt upgrade every week.
Frequently asked questions
Will unattended-upgrades update my kernel automatically?
Yes, if a kernel security patch lands in the security repo, it gets downloaded and installed like any other package - but the running kernel only changes after a reboot. With Automatic-Reboot left off, the server keeps running the old kernel in memory until you reboot it yourself; the /var/run/reboot-required flag tells you it's waiting.
Is this safe to run on a cPanel or WHM VPS?
Yes, as long as you blacklist the packages cPanel manages itself, like Apache, MySQL/MariaDB, and PHP builds installed through EasyApache. cPanel expects to control those versions through WHM, and having apt patch them independently can cause version mismatches. Stick to OS-level security packages (openssl, sudo, glibc, kernel) and leave the hosting stack to WHM's own update settings.
How do I know if unattended-upgrades is actually running?
Check systemctl status apt-daily-upgrade.timer and apt-daily.timer - both should show as active. Then look at /var/log/unattended-upgrades/unattended-upgrades.log for recent entries. If the log is empty or stale, the timers likely aren't firing; systemctl list-timers will show you the next scheduled run.
What's the difference between this and just running apt upgrade in a cron job?
A plain cron job with apt upgrade -y pulls in every available update, including non-security bug fixes and anything not on a blacklist - which is how you end up with an unplanned MySQL or PHP version bump. unattended-upgrades scopes itself to a defined origin (security-only by default), respects a package blacklist, and logs its decisions, so it's a controlled patch process rather than a blind one.
My reboot-required flag never clears even after rebooting. What's wrong?
That usually means a second update landed between your reboot and the flag check, or a package left a stale marker in /var/lib/update-notifier/. Manually remove /var/run/reboot-required and /var/run/reboot-required.pkgs after confirming uname -r matches the newest installed kernel, then let the next patch cycle recreate the flag only if it's genuinely needed again.