All articles
TutorialsJul 17, 2026 · 27 min read · By The RDP.sh Team

Automatic Security Updates on Ubuntu with Unattended-Upgrades

Automatic Security Updates on Ubuntu with Unattended-Upgrades

Almost every server that gets popped was running a version of something with a known, patched CVE. Not a zero-day. A bug that had a fix available for weeks, sitting in the security pocket, waiting for somebody to type apt upgrade. Nobody typed it, because typing it is boring and there was always something more urgent.

unattended-upgrades fixes the boring part. It ships with Ubuntu, it costs no memory worth measuring, and once it is configured properly you stop thinking about security patches entirely. The catch is that "configured properly" is doing a lot of work in that sentence. The default install downloads and applies security updates but never reboots, so your kernel patches sit unused for months. And on 22.04 and newer, needrestart will happily pause an automated run waiting for a human to answer a question about restarting services.

This post sets it up the way you actually want it: security patches applied nightly, a reboot window you chose, services restarted without prompting, and a notification when something changes.

TL;DR

  • sudo apt install unattended-upgrades - it is likely already there
  • Enable it with sudo dpkg-reconfigure --priority=low unattended-upgrades, which writes 20auto-upgrades
  • Keep Allowed-Origins to the -security pocket only, leave -updates commented out
  • Set Automatic-Reboot "true" with Automatic-Reboot-Time "03:00" or kernel fixes never load
  • Set needrestart to $nrconf{restart} = 'a' or automated runs can hang on a prompt
  • Blacklist anything you upgrade by hand (databases, Docker) so it never moves under you
  • Verify with sudo unattended-upgrade --dry-run --debug before trusting it
  • Check /var/log/unattended-upgrades/unattended-upgrades.log after the first night

Total time: about 20 minutes.

What You Need

  • A VPS running Ubuntu 22.04 or 24.04 (Debian works too, with one config difference noted in Step 3)
  • A sudo user, ideally set up per our SSH hardening guide
  • An idea of which hour of the day your server is least busy
  • Optional: a working mail setup if you want emailed reports

Nothing here needs Docker, and it uses essentially no RAM - the process runs once a day and exits.

Step 1: See What You Are Actually Missing

Before automating anything, look at the current state. This is usually the moment people realize how far behind they are.

sudo apt update apt list --upgradable

To see only the security ones:

apt list --upgradable 2>/dev/null | grep -i security

And check whether a reboot is already pending from some previous upgrade:

ls -l /var/run/reboot-required cat /var/run/reboot-required.pkgs

If that file exists, your running kernel is not the kernel you have installed. Get to a clean baseline first:

sudo apt full-upgrade -y sudo apt autoremove -y sudo reboot

Start automation from a known-good state, not from a six-month backlog. If a mass upgrade is going to break something, you want it to break now, while you are watching, rather than at 3am next Tuesday.

Step 2: Install and Enable

The package is usually preinstalled on Ubuntu server images, but make sure:

sudo apt install unattended-upgrades apt-listchanges -y

apt-listchanges is optional but nice - it puts the changelog of what was upgraded into the report email.

Now enable it:

sudo dpkg-reconfigure --priority=low unattended-upgrades

Answer yes. All that does is write /etc/apt/apt.conf.d/20auto-upgrades. You can also just write the file yourself:

sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::AutocleanInterval "7"; EOF

What those mean:

  • Update-Package-Lists "1" - run apt update daily
  • Download-Upgradeable-Packages "1" - pre-download so the upgrade window is short
  • Unattended-Upgrade "1" - actually apply them daily
  • AutocleanInterval "7" - clear the old .deb cache weekly so /var does not fill up

The numbers are days, not booleans. "7" means every seven days. "0" disables.

Confirm the values took:

apt-config dump APT::Periodic

Step 3: Choose What Gets Upgraded

This is the file that matters: /etc/apt/apt.conf.d/50unattended-upgrades. Open it:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

The top block controls which repositories are allowed to push updates at you unattended. The Ubuntu default looks like this:

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"; };

Leave this alone. Seriously. The default is the right answer for a server: security fixes only.

The temptation is to uncomment -updates so you get everything. Resist it. The -security pocket is a narrow, heavily-tested stream of fixes where Ubuntu goes out of its way not to change behavior - they backport the patch into the old version rather than bump you to a new one. -updates is a much broader stream of bug fixes and feature changes, and that is where unattended upgrades start surprising you. Apply -updates by hand, on a day you are paying attention.

On Debian, this block does not exist. Debian uses `Unattended-Upgrade::Origins-Pattern` with entries like `"origin=Debian,codename=${distro_codename},label=Debian-Security";` instead. The rest of this post applies unchanged - only the origins syntax differs. Do not paste Ubuntu's `Allowed-Origins` into a Debian box and expect it to match anything.

Blacklist the things you own

Anything you upgrade deliberately - a database, Docker, a runtime your app is pinned against - should never move on its own at 3am. Find the Package-Blacklist block and fill it in:

Unattended-Upgrade::Package-Blacklist { "docker-ce"; "docker-ce-cli"; "containerd.io"; "mariadb-server"; "postgresql-16"; };

Entries are regular expressions matched against the package name, and they are unanchored. "postgres" would match every package with "postgres" anywhere in the name, which is probably not what you want. Be specific, and anchor with ^...$ if you need exactness:

Unattended-Upgrade::Package-Blacklist { "^docker-ce$"; "^linux-image-.*$"; };

Blacklisting kernels is a real choice some people make for hosts where an unplanned reboot is worse than a delayed patch. If you do that, you are signing up to patch kernels manually. Put it in a calendar.

Useful options further down

These are all in the same file, mostly commented out by default:

Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; Unattended-Upgrade::Remove-Unused-Dependencies "false"; Unattended-Upgrade::MinimalSteps "true"; Unattended-Upgrade::SyslogEnable "true";

Remove-Unused-Kernel-Packages "true" matters more than it looks. /boot on a VPS is often a small partition, and Ubuntu keeps old kernels around. Left alone for a year, /boot fills, the next kernel upgrade fails halfway, and you get to debug a broken initramfs on a console at an unpleasant hour. Let it clean up after itself.

MinimalSteps "true" splits the upgrade into the smallest independent chunks it can, so if the run gets interrupted (or hits the timeout) you are left with a consistent dpkg state rather than a half-configured mess.

Step 4: Handle Reboots on Purpose

Here is the part that makes the difference between "I have automatic updates" and "I am actually patched."

A kernel security fix is a file on disk. It does nothing until you boot into it. The default unattended-upgrades config never reboots, so on a stock setup your kernel CVEs get downloaded, installed, and then ignored indefinitely while /var/run/reboot-required quietly accumulates.

You have two honest options. Pick one.

Option A: reboot on a schedule you chose.

Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-WithUsers "true"; Unattended-Upgrade::Automatic-Reboot-Time "03:00";

It only reboots when /var/run/reboot-required exists, so this is not a nightly reboot - it is a reboot on the handful of nights a kernel or core library actually changed. Automatic-Reboot-WithUsers "true" means it reboots even if someone is logged in over SSH; set it to "false" if you would rather a stray screen session block the reboot forever, which is usually the wrong tradeoff on a server.

Pick a real time, in the server's timezone. Check what that is:

timedatectl

If your VPS is on UTC and you are in Berlin, 03:00 is 5am local in summer. Fine for most people, but know which one you picked.

Option B: do not auto-reboot, and monitor for pending reboots instead.

Leave Automatic-Reboot "false" and make the pending-reboot state visible so it does not rot. A one-line check you can wire into any monitoring:

test -f /var/run/reboot-required && echo "reboot pending: $(cat /var/run/reboot-required.pkgs | tr '\n' ' ')"

Point Uptime Kuma or a Grafana dashboard at it, or have it push to ntfy. The rule: if you choose Option B, you must build the reminder. Option B without monitoring is just Option "never reboot", and that is the failure mode this whole post exists to prevent.

If your VPS runs one stateful thing - a database, a game server, a build box - test that it actually comes back up on its own before you enable automatic reboots. Run `sudo systemctl is-enabled yourservice` for everything that matters, then do one deliberate `sudo reboot` and watch. A 3am reboot is only safe if boot-time recovery is safe, and the time to learn that a service was never `enable`d is a Tuesday afternoon, not a Sunday night.

Step 5: Tame needrestart

On Ubuntu 22.04 and newer, needrestart is installed by default. When a library like openssl or glibc is patched, the running processes that linked it are still using the old copy in memory. needrestart spots this and restarts the affected services - which is exactly what you want, because otherwise you patched the file and left the vulnerable code running.

The problem is its default mode is interactive. It wants to ask you which services to restart. In an automated run that means the process either hangs or bails, depending on the version and how it was invoked.

Fix it:

sudo nano /etc/needrestart/needrestart.conf

Find and set:

$nrconf{restart} = 'a';

'a' means automatic, 'i' is interactive (the default), 'l' means list-only and restart nothing. You want 'a' on a server.

While you are there, this line controls the kernel-upgrade dialog:

$nrconf{kernelhints} = -1;

-1 suppresses the "pending kernel upgrade" prompt. You do not need it, because Step 4 already handles reboots deliberately.

If you would rather not edit the main config, drop an override instead - it survives package upgrades more cleanly:

sudo tee /etc/needrestart/conf.d/99-unattended.conf > /dev/null <<'EOF' $nrconf{restart} = 'a'; $nrconf{kernelhints} = -1; EOF

Note the file must be valid Perl, which is why the lines end in semicolons.

Step 6: Pick the Window

Two systemd timers drive the whole thing:

systemctl list-timers 'apt-daily*'
  • apt-daily.timer - refreshes package lists and downloads
  • apt-daily-upgrade.timer - runs the actual unattended upgrade

By default apt-daily-upgrade fires around 06:00 with a randomized delay of up to an hour, which exists so that ten thousand VPSs do not hammer the mirrors at the same second. If that clashes with your traffic or your backup window, override it:

sudo systemctl edit apt-daily-upgrade.timer

Add:

[Timer] OnCalendar= OnCalendar=02:30 RandomizedDelaySec=30m

The empty OnCalendar= first is not a typo. systemd list-type settings accumulate, so without the empty line to reset it you end up with both the old schedule and the new one. Then:

sudo systemctl daemon-reload systemctl list-timers apt-daily-upgrade.timer

Sequence the times so they make sense together: package lists refresh, then upgrades run, then - if a reboot is needed - it happens at Automatic-Reboot-Time. Set the reboot time after the upgrade window, not before, or the reboot fires on the previous day's pending flag and the actual upgrade waits another 24 hours. 02:30 upgrade and 03:00 reboot works. 03:00 upgrade and 02:30 reboot does not.

Step 7: Dry Run Before You Trust It

Do not wait until tomorrow morning to find out it is broken:

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

This changes nothing and prints exactly what it would have done. Read the output. You are looking for three things:

  1. Allowed origins are: ... - the origins it resolved. If your codename is wrong or the pocket does not exist, you will see it here.
  2. Checking: <package> lines with is blacklisted next to anything you blacklisted in Step 3. Confirm your regexes match what you think they match.
  3. Packages that will be upgraded: ... at the end, or No packages found that can be upgraded unattended if you are current.

That second point is worth doing carefully. A blacklist regex that silently matches nothing is a very quiet failure - everything looks fine right up until the night your database gets a minor version bump.

To force one real run right now, without waiting for the timer:

sudo unattended-upgrade -v

Then read the log:

sudo tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log

You can also trigger the whole systemd path end to end, which tests the unit and not just the binary:

sudo systemctl start apt-daily-upgrade.service journalctl -u apt-daily-upgrade.service -n 50 --no-pager

Step 8: Get Told When Something Happens

Silent automation is how you end up with a service that has been failing for four months.

If you have working mail on the box (see our Postfix relay guide - a VPS almost never delivers direct-to-inbox), set:

Unattended-Upgrade::Mail "[email protected]"; Unattended-Upgrade::MailReport "on-change";

MailReport takes always, on-change, or only-on-error. on-change is the right default: you hear about it when something moved, and you are not training yourself to ignore a nightly "nothing happened" email.

No mail server and no interest in running one? Ping a dead-man's-switch instead. Add a drop-in on the upgrade service:

sudo systemctl edit apt-daily-upgrade.service [Service] ExecStartPost=-/usr/bin/curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-UUID-HERE

The leading - on ExecStartPost= means a failed ping does not mark the unit failed - you do not want a monitoring hiccup to look like a broken upgrade. Now Healthchecks.io (or a self-hosted equivalent) alerts you when the run stops happening, which is the failure you are least likely to notice on your own.

Troubleshooting

unattended-upgrade --dry-run says "No packages found that can be upgraded unattended" but apt list --upgradable shows plenty. Working as intended, almost certainly. Those pending packages are in -updates, not -security. Check with apt-cache policy <package> and look at which pocket the candidate comes from.

The run dies with "Could not get lock /var/lib/dpkg/lock-frontend". Another apt process was running - often apt-daily.service overlapping with your manual command. Wait for it: systemd-run --property="After=apt-daily.service" --wait /bin/true. If it is stuck permanently, find the holder with sudo lsof /var/lib/dpkg/lock-frontend rather than deleting the lock file.

Upgrades apply but the server never reboots. Either Automatic-Reboot is still "false", or /var/run/reboot-required does not exist because nothing needing a reboot was upgraded, or Automatic-Reboot-WithUsers is "false" and you have a logged-in session. Check all three in that order.

A run hangs forever and the timer never completes. This is needrestart in interactive mode - go back to Step 5. Confirm with grep -r 'nrconf{restart}' /etc/needrestart/.

/boot is full and upgrades fail. Set Remove-Unused-Kernel-Packages "true", then clear the backlog by hand: sudo apt autoremove --purge. Check what is left with dpkg -l 'linux-image-*' | grep ^ii. Never rm files out of /boot directly - dpkg still thinks they are there, and you will end up hand-repairing its state.

Config changes did not take effect. Ordering matters: files in /etc/apt/apt.conf.d/ are read in lexical order, and the last assignment wins. A stray 52unattended-upgrades-local overrides 50unattended-upgrades. Ask apt what it actually believes with apt-config dump | grep -i unattended.

Going Further

  • Layer it with the rest of the basics. Automatic patching is one control. Key-only SSH, a firewall, and Fail2Ban are the others - our SSH hardening guide covers those, and CrowdSec adds shared threat intelligence on top.
  • Remember that containers are not covered. unattended-upgrades patches the host. The Debian userland inside your Docker images is frozen at build time and gets patched by rebuilding and redeploying, not by anything here. If you self-host with Compose, pair this with Watchtower or a scheduled docker compose pull && docker compose up -d, and see our Docker security guide.
  • Consider livepatch for kernel CVEs. Ubuntu Pro is free for personal use on up to five machines and includes Livepatch, which applies kernel security fixes to the running kernel with no reboot. It does not remove the need to reboot eventually, but it closes the window between "patch exists" and "patch is live" on servers where reboots are genuinely expensive.
  • Roll it out consistently. The setup above is a handful of files. Put them in Ansible, a cloud-init snippet, or your provisioning script so every server you build starts patched instead of getting it retrofitted six months later when you remember.

Automatic security updates are the highest ratio of protection to effort available on a Linux server. Twenty minutes once, and the class of attack that accounts for most real compromises - a known bug with a published fix - stops being your problem. The only work left is picking a reboot window and reading the occasional email.


Need a VPS to run this on? Our Linux VPS plans ship with clean Ubuntu 22.04 and 24.04 images, NVMe storage, and a web console for the night an upgrade goes sideways. See the options.