Repository navigation
Patch Management and Automatic Security Updates
Status: partly run, mostly written from reading. On 2026-10-30 I ran three things inside throwaway Ubuntu 24.04.5 containers (rootless Podman,
unattended-upgrades2.9.1+nmu4ubuntu1, network on): the dry run in Exercise 1, how a second configuration file changes the list of allowed origins (measured, in "Configure it"), and the triage script in Exercise 2 against both hand-made logs and the dry-run log. Everything else was not run and is written from reading: the RPM-family tool, plain Debian, the daily timers firing on a real host, a real automatic reboot, every rollback step, and Patch Gremlin (I read its README on the same day, not its code, and installed nothing). The vendor pages were read on 2026-10-30; each is in the claims register with what I took from it. Where a statement is general knowledge and not on a page I read, the text says est. (estimate). The staging stack in the "Testing a patch" section is planned and not built.
Unfamiliar word or acronym? The Glossary defines it.
Home > Track 1: Foundations > Patch management and automatic security updates
Previous: First hour on a new system / Next: Red and blue, and choosing your lab OS
On this page:
- What you will be able to do
- Why patch automatically
- What unattended-upgrades does
- Configure it
- Where the results are logged
- What deserves an alert
- Reboot handling
- Testing a patch before production
- Rolling back
- The limits of automatic updating
- Free path: a container, a log and a script
- Predict, then check
- Try it yourself
- Claims register
Prerequisites: First hour on a new system (why updates come first, the table of mechanisms for every system, and the staggered windows) and Backups, recovery and patching (cadence, emergency patches, and a backup you have restored). Logs, SIEM and detection basics helps with the alerting half. To run the exercises you need Podman or Docker on a Linux or macOS computer; they cost nothing and touch no cloud.
Those two pages say that you should patch and give the vocabulary. This page is about the machinery on one Debian or Ubuntu host and the part people leave out: how you find out that it worked, that it broke, or that it quietly stopped.
- Say what
unattended-upgradesdoes in one daily run, and which files and timers control it. - Read its configuration and say which repositories it will and will not take updates from.
- Name where each kind of update result is logged.
- List the four conditions that deserve an alert and say which one people miss.
- Choose a reboot policy and say what it costs.
- Describe how a patch is tested before production, and how you go back when one goes wrong.
- Say what automatic updating does not cover.
- Say what Patch Gremlin does and does not do, from its README.
A vendor fixes a flaw and publishes the fix. From that moment the flaw is public, and the only question is how long your machine stays unfixed. Attackers do not need to be clever: they scan for the known-bad version. The time between "fix published" and "fix installed on your host" is the window they use. A person who patches "when I remember" leaves that window open for weeks (est.); a daily automatic job closes it in about a day.
The price is real. An update can break a service, and a reboot can fail to come back. Nothing in this lesson removes that price; it makes it smaller and visible. The rest of the page is the four habits that do it: limit what is automatic (security fixes only), make the result loud (logs and alerts), try it somewhere cheap first (staging and a canary), and know the way back (rollback). The comparison to make in writing is the one First hour on a new system makes: the risk of an update breaking something against the risk of leaving a known flaw open. "It might break" is a reason for these habits, not for staying unpatched.
unattended-upgrades is a package for Debian and Ubuntu. Ubuntu's server documentation says Ubuntu "will apply security updates automatically, without user interaction", through this package, "installed by default". It does the equivalent of apt update followed by apt upgrade: it refreshes the package lists, checks which upgrades are available, and applies those from the allowed repositories. By default it runs once a day. Its man page calls it the backend for APT::Periodic::Unattended-Upgrade, run by APT's systemd service apt-daily-upgrade.service or from cron.
Three pieces make it go:
-
Two systemd timers,
apt-daily.timer(downloads) andapt-daily-upgrade.timer(upgrades), start the work at a scheduled time plus a random delay. The Debian wiki explains the delay: before it existed, Debian's servers got "massive traffic spikes for a few seconds a day", so if you move the time, keep a delay of at least an hour. If the machine was off at the scheduled time, the timer fires at the next start, which is why a freshly booted VM often upgrades itself at once (Ubuntu page). The setting that stops that catch-up isPersistent=falsein a timer override. -
/etc/apt/apt.conf.d/20auto-upgradesswitches the work on. Two lines, each a number of days;0disables,1is daily,2is every other day:APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; -
/etc/apt/apt.conf.d/50unattended-upgradessays what it may do: which repositories, which packages to skip, whether to reboot, whether to send mail.
Plain Debian differs from Ubuntu in one respect worth knowing. The Debian wiki says "most versions of Debian install unattended-upgrades with fairly conservative settings" but that your system "might not have installed the package at all, or might have installed it but disabled it altogether", and tells you to check with sudo dpkg-reconfigure unattended-upgrades. Its example configuration works on a copy named 52unattended-upgrades-local and refers to an Origins-Pattern section, where Ubuntu's page documents Allowed-Origins. Read the page for your release and look at your own 50 file; do not assume the two are the same (I ran only Ubuntu 24.04.5).
The same job on the RPM family is dnf-automatic. It is not set up for you: per its documentation, apply_updates defaults to False (it downloads and reports), upgrade_type can be security, and reboot is never, when-changed or when-needed. The table in First hour on a new system lines up every mechanism; this page stays on Debian and Ubuntu and notes the RPM equivalent where it matters.
Do not edit the 50 file. Put your settings in a file that sorts after it, as the Debian wiki suggests (it names 52unattended-upgrades-local), so a package upgrade of unattended-upgrades cannot overwrite them. This project does the same, in the order its stage guides state (update and switch on automatic security updates, write the baseline, switch on the hardening, install the application last): its bootstrap writes 52cah-unattended-upgrades next to 20auto-upgrades (modules/twingate-bootstrap/cloud-init.yaml.tftpl, read in this repository).
| Option | What it does | Default or note | Source |
|---|---|---|---|
Unattended-Upgrade::Allowed-Origins |
Repositories updates may come from. Written "origin:suite". |
The release itself, its -security pocket, and Ubuntu's ESM (Expanded Security Maintenance) pockets where they exist. -updates is commented out. |
Ubuntu page; confirmed in the container |
Unattended-Upgrade::Package-Blacklist |
Packages never upgraded automatically; a list of Python regular expressions, matched as if anchored at the start. "libc6$" matches libc6 and not glibc6. |
Empty. Blocking a package can block updates that depend on it. | Ubuntu page |
Unattended-Upgrade::Mail, MailReport
|
Send a report to an address. MailReport is always, only-on-error or on-change. |
No mail unless Mail is set; on-change by default. Needs a mail program on the host. |
Ubuntu page |
Unattended-Upgrade::Automatic-Reboot |
Reboot at the end of a run if an update asks for it. | false |
Ubuntu page |
Unattended-Upgrade::Automatic-Reboot-WithUsers |
Reboot even if someone is logged in. |
true (only matters when the line above is true) |
Ubuntu page |
Unattended-Upgrade::Automatic-Reboot-Time |
When to reboot. Passed as is to shutdown(8): now, hh:mm in the machine's local time, or +m minutes. |
now |
Ubuntu page |
Unattended-Upgrade::OnlyOnACPower |
A laptop setting: by default the run is skipped on battery. | The Debian wiki warns the skipped run produces no notification. | Debian wiki |
Which repositories it trusts is the most important line. Ubuntu's page is blunt: adding another package repository "WILL NOT make unattended-upgrades consider it for updates". That is a safety feature (a third-party repository is not auto-installed as root) and a gap (the software from that repository is never updated automatically). To include one, add its Origin from its InRelease file to the list. Take the gap seriously: the Twingate connector comes from Twingate's own repository, so it is not covered by the security origins; this project patches it with its own weekly cron job (Droplet hardening: updates).
A second file adds to a list; it does not replace it. I measured this on 2026-10-30 in an Ubuntu 24.04.5 container. With the stock 50 file in place, I added a 52 file that named only the -security origin, and asked apt-config dump for the result. The output held the four stock origins and the security origin again. The tool's own log then listed the five. So a 52 file that restates a shorter list does not restrict anything; it only adds. To actually replace the list, put #clear Unattended-Upgrade::Allowed-Origins; on the first line of your file; with that line the dump showed an empty entry followed by only the origin I wrote (also measured, one run). Why it matters: this project's 52 file lists the security origins, and the stock list still holds the base release pocket. That pocket carries the packages from release day, not later updates, so the practical difference is small (est.), but a page that says "only security origins" should be read as "at least the security origins".
A correct, minimal local file for a server that wants security fixes only and a nightly reboot window looks like this (the same shape as this project's):
// /etc/apt/apt.conf.d/52local-updates
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";
This is an example, not a recommendation for your host. Every line has a cost: WithUsers "true" will reboot a machine someone is working on, and Mail does nothing until a mail program is installed. The Debian wiki's debug command checks your file without waiting for the timer: sudo unattended-upgrade -d (and --dry-run simulates).
Know these before you need them. All paths are Debian or Ubuntu; the first two I saw created by a run in the container.
| What you want to know | Where | Source |
|---|---|---|
What the tool decided and did, with a timestamp and a level (INFO, WARNING, ERROR, DEBUG): the allowed origins, "Packages that will be upgraded", "All upgrades installed", reboot messages |
/var/log/unattended-upgrades/unattended-upgrades.log |
unattended-upgrade(8); saw it in the container |
What dpkg printed while installing |
/var/log/unattended-upgrades/unattended-upgrades-dpkg.log |
unattended-upgrade(8); saw it in the container |
| Every package change on the host, by any tool, by hand or automatic | /var/log/dpkg.log |
Debian wiki |
| Whether the timer and its service ran, and what the service wrote to the journal |
journalctl -u apt-daily-upgrade.service and systemctl list-timers apt-daily*
|
est.: standard systemd commands; the unit name is from the man page, the commands were not run |
| Whether a reboot is waiting | the file /var/run/reboot-required
|
Ubuntu page (the mechanism is described, the path appears in its log excerpt: "Found /var/run/reboot-required, rebooting") |
| A mailed report of one run | the address in Unattended-Upgrade::Mail, subject like "unattended-upgrades result for : SUCCESS"
|
Ubuntu page |
| Patch Gremlin's own record |
journalctl -t patch-gremlin, the file /var/lib/patch-gremlin/state
|
Patch Gremlin README |
One consequence: the log file only helps you if you read it, and it lives on the host. If a host is compromised or destroyed, its log goes with it. Shipping it to a collector is the point of Logs, SIEM and detection basics; this project's droplets do not ship these logs yet (audit logging is planned for stage 2, see Droplet hardening).
Log files rotate (est.), so "the newest run" is not "the whole file". The exercise below handles that.
An alert is a message to a person who should do something. Four conditions earn one. Everything else is a log line.
| Condition | Why a person must hear about it | How you can tell | Urgency |
|---|---|---|---|
| A security update was applied | It is what you wanted, and it is also a change on a production host. You want a record that tomorrow's odd behavior has a cause. | "Packages that will be upgraded" followed by "All upgrades installed" in the log; the mail with on-change; Patch Gremlin's notification |
Notice: no one is paged; it is read in the morning |
| A reboot is required | A kernel or C library fix does nothing until the reboot. A host that has been waiting a week is a host with a known flaw. |
/var/run/reboot-required exists; the log says it is rebooting |
Warning: fine if your window handles it, not fine after a few days |
| An update failed | A half-applied update can leave a package broken, and the next run may fail the same way. | An ERROR line in the newest run; only-on-error mail |
Page during working hours; the fix is a person looking |
| The host stopped updating | Silence looks like success. A host with a stuck lock, a full disk, a broken repository, a disabled timer or a turned-off job logs nothing and sends nothing. | The newest run is too old; no run at all | The one most worth waking someone for, because nothing else will tell you |
The fourth row is the point of this section. The first three are events and every tool reports events. The fourth is the absence of an event, and a tool that reports only events cannot report it. You need something that notices a number going stale: a "last successful run" age compared with a limit. Grafana basics has the same trap in its no-data setting, and the synthetic lab has a rule that counts expected targets for the same reason.
How the alert reaches a person is a separate decision from what triggers it, and each hop can fail silently:
- Detection on the host or in a collector: a script like the one in Exercise 2, a mail report, or Patch Gremlin.
- Delivery to somewhere a person looks: email, or a chat webhook. Test it with a real message; a webhook that was revoked fails without telling anyone.
- Routing: notices to a quiet channel, failures and staleness to the one that pages. If every run posts a message, people mute the channel, and the mute hides the failure.
- A second path for the alert path itself. If the notifier is the only thing that says the host stopped updating, who says the notifier stopped? A heartbeat or a central check that reads the last-run time from outside the host is the usual answer (est.).
Patch Gremlin is the maintainer's own project, public at github.com/ChiefGyk3D/Patch-Gremlin. Everything here comes from its README as I read it on 2026-10-30; I did not read its code, install it, or run its tests.
What the README says it is: automated update notifications for Debian and RHEL-based systems. It integrates with unattended-upgrades on Debian, Ubuntu and Raspbian, and with dnf-automatic on RHEL, Rocky, Alma, Amazon Linux and Fedora, and sends a notification when security updates are installed. Destinations it lists: Discord, Microsoft Teams, Slack, Matrix, ntfy, Gotify, and a generic JSON webhook, in any combination.
How it maps onto the four conditions:
| Condition | What the README describes |
|---|---|
| Update applied | It reports a run as "N packages updated", "still pending (some security)" or "up to date", with the package names (the number of names listed is a setting). It hooks the upgrade service itself (ExecStartPost on apt-daily-upgrade.service or dnf-automatic.service) so the message describes the run that just finished. |
| Reboot required | An optional auto-reboot, with a reboot window it derives from your update time. The setup prompts for it and recommends it; it reboots after kernel or critical library updates and, when enabled, even with users logged in (the README's words). I found no separate "reboot pending" notification in the part I read. |
| Update failed | It logs success or failure of its own delivery to the system log and retries a failed send. Whether an unattended-upgrades ERROR becomes its own message I did not find stated. |
| Host stopped updating | A "heartbeat" report, disabled by default (ENABLE_HEARTBEAT=true turns it on), scheduled two hours after the upgrade window, for the days when no upgrade ran. A health check that exits 0, 1 or 2, a Nagios/Icinga check whose sample output shows last_run_age_hours, and a Prometheus exporter with a patch_gremlin_last_run_timestamp_seconds metric. |
The last row is the useful one. The Prometheus metric is a last-run time, so the stale-host rule is one comparison. As an example only (not run, no Prometheus here; the 36-hour limit is my choice, a day plus a margin):
time() - patch_gremlin_last_run_timestamp_seconds > 36 * 3600
What to remember about it:
- It reports what the host's package manager did. It does not look inside a container image or at your application's own dependencies. Backups, recovery and patching has the table.
- It is another thing that holds a secret (a webhook address or a token). The README says it keeps them in a root-only mode 600 file or in Doppler, the same rule as Secrets from day one. A webhook URL in a chat channel is a password for posting to that channel.
-
It writes configuration into the same files this lesson shows (the README's file list names
/etc/apt/apt.conf.d/50unattended-upgrades, a drop-in on the upgrade timer for the schedule, and20auto-upgrades). If you manage those with a configuration tool or cloud-init, give one of them ownership, or each will undo the other (est.). - The README describes the trigger in two places that read differently: the Configuration section says a hook on the upgrade service, the How It Works section says an APT post-invoke hook. The file list shows the service drop-in. I cannot say which is current without reading the code.
- License: the README says dual MPL-2.0 or commercial; read it before reusing it in a company.
- The README's own notice says version 2.0 changed how notifications are triggered and fixed a credential-exposure issue in 1.x; if you find an older copy, update it first.
This project does not install it on its droplets. It is the example of the alert path, not part of the reference environment.
Some updates cannot take effect until the machine restarts. The kernel is the main one; Ubuntu's page says an update can "let the system know that a reboot is recommended" and that unattended-upgrades "can benefit from this mechanism and optionally reboot". It also says "Reboots can be very disruptive, especially if the system fails to come back." Your policy is one of three.
| Policy | How | Good for | Cost |
|---|---|---|---|
| Never automatic (the default) |
Automatic-Reboot "false"; you watch for the reboot-required alert |
Machines where a person owns each restart: a database, a router, a desktop | The flaw stays open until a person acts. Alert on how long the flag has been set. |
| Automatic in a window |
Automatic-Reboot "true" and Automatic-Reboot-Time "hh:mm"
|
Stateless or redundant machines; this project's droplets | A reboot at that time, whether or not you are watching. If the machine fails to come back, you find out at that time. |
| Replace, do not patch | Build a new instance from a newer image and destroy the old one | Cloud machines designed to be recreated | You need the pipeline for it. Ubuntu's page notes the exposure is the same: "the update still has to happen somewhere, it's just not at runtime." |
Three details that bite:
-
The time is local to the machine.
hh:mmis read in the machine's time zone. This project sets the droplet clock from itstimezonesetting (defaultAmerica/New_York), so its times are Eastern (Droplet hardening: updates). A machine left on UTC reboots at a different hour than you meant. -
Not everything needs a reboot, but it needs a restart. After a library update, running services keep the old copy in memory. The package
needrestart, whichunattended-upgradescalls, restarts those services; the Ubuntu page says that starting with Ubuntu 24.04 LTS it does so automatically by default, except for a built-in list. You can set$nrconf{restart}to'l'(list only) or exclude one critical service with$nrconf{override_rc}, in a file under/etc/needrestart/conf.d/, and restart it in a window. The Debian wiki points at the same package for processes that need restarting without a reboot. -
Never reboot the redundant machines together. This project has two Twingate connectors so that one can be down; they reboot an hour apart (04:00 and 05:00 Eastern by default), and a validation in the access stack refuses two reboot times less than 60 minutes apart (
stacks/10-access/variables.tf, read in this repository). The planned edge host takes one of five 30-minute slots from 01:00 to 03:00, each service its own, listed inmodules/edge-host/reboot-slots.json, before the connectors' windows. First hour on a new system explains why the stagger is also your canary.
With dnf-automatic, the equivalent is reboot = when-needed, and the command defaults to shutdown -r +5 'Rebooting after applying package updates': a five-minute warning, which you can shorten (DNF Automatic page).
Automatic security updates are the one class you let reach production without a human, so the testing happens in two other places.
-
The upstream test. The vendor's security pocket holds small, targeted fixes. That is why "security only" is the automatic default: the change is small, and the vendor has already tested it. A full update (
-updates) is bigger and belongs in a window with a person watching (First hour page). -
A dry run on the host.
sudo unattended-upgrade --dry-run(add-dfor detail) shows what it would install. It does not prove the new versions work; it proves the configuration reads and the origins match. Exercise 1. - A canary. The first machine in the window is the test for the rest. A staggered schedule gives you one by accident; naming one on purpose is better.
- A staging copy. A machine built the same way as production, updated first, watched for a day. This project does not have one yet: the staging stack is issue #310, a separate Spacelift stack and DigitalOcean project, planned for sprint S7 and built only when the maintainer sets up its accounts (Roadmap). Until then, a connector droplet's own rebuild from cloud-init is the closest thing to a test, and it is not one: both connectors share the same template.
- The test that matters: did the service come back? After the window, check the thing the host is for, not the update tool. For a connector that means it shows online in Twingate. An update log that says "All upgrades installed" and a service that is down is a failed patch.
Be honest about what apt gives you: no transaction undo (est.; nothing on the pages I read describes one). So rollback is one of these, in order of how much you can rely on them:
| Method | What it needs | Honest limit |
|---|---|---|
| Restore a snapshot or boot environment taken before the window | A snapshot habit (Backups, recovery and patching) | A snapshot restores the disk, including data written since. Time-box the decision. |
| Rebuild from the code | The machine is defined as code and holds no state that matters. This project's droplets are exactly this: change user_data and the droplet is replaced (Droplet hardening). |
A rebuild installs today's versions, so it does not take you back to the old package. It gets you a clean host, which is often what you wanted. |
| Install the previous version of one package | The old version still in the archive; apt install package=version, then hold it (apt-mark hold) (est.; commands not run) |
The fix you are avoiding is gone too. Put an owner and an expiry date on the hold, or it becomes a quiet vulnerability. |
| Stop the next run |
Package-Blacklist for one package, or APT::Periodic::Unattended-Upgrade "0" for all |
Stops the problem getting worse, but does not undo it. A blocked package can block its dependents (Ubuntu page). |
A rollback you have never tried is a hope. Pick one machine and try the method you would use, once, before you need it.
For the general rule, Ubuntu's own page lists when to disable or limit automatic updates: systems that are rebuilt anyway; where a manual step is needed before or after an update (block those packages instead of the whole feature); and where the risk of changing outweighs the risk of staying put. Its middle ground for the last case is to block specific libraries rather than everything.
Automatic security updates are necessary and not sufficient. What they miss:
- Software from other repositories. Not considered unless you add the origin. The connector here is the example.
- Containers. The host's updater does not look inside an image. Backups, recovery and patching covers this.
- Application dependencies (a Python, JavaScript or PHP package that your code pulls in). Those need a different tool and a person who owns the update.
- Anything waiting on a reboot. An installed kernel fix is not a running kernel fix.
- Machines that are off, or not on mains power. A timer that fires while a laptop is off runs later; a laptop on battery skips the run and, per the Debian wiki, "you're not notified about it".
- A broken updater. A locked package database, a full disk or a dead repository stops the job. This is the fourth alert in the table above.
- Fleet questions. Ubuntu's page says the feature is not "a replacement for fleet management software" when many machines need compliance reports ("how many systems are up-to-date, how many are still behind"), different windows, and canary rollouts, and names Landscape as an example for Ubuntu. If you can no longer answer "which of my machines is behind?" in one query, you have outgrown it.
- Appliances (routers, firewalls) are updated by a person on purpose; see the First hour table.
-
Emergencies. A flaw being exploited now cannot wait for tomorrow's timer. You need the pre-agreed fast path (Enterprise change management), and the means to run
apton every host at once.
Cost: $0. Everything runs in a throwaway container and deletes itself. You need Podman or Docker (podman and docker are interchangeable below; Docker may need sudo). Nothing here installs anything on your real computer, sends anything, or touches anyone else's machine.
Predict. If you install unattended-upgrades in a fresh container and run a dry run, will it install anything? Which file will say what it did, and what will the first lines of its log list?
Do.
podman run --rm docker.io/library/ubuntu:24.04 bash -c '
apt-get update -q >/dev/null 2>&1 && apt-get install -y -q unattended-upgrades >/dev/null 2>&1
cat /etc/apt/apt.conf.d/20auto-upgrades
unattended-upgrade --dry-run >/dev/null 2>&1
ls -l /var/log/unattended-upgrades/
grep -E "Starting|Allowed origins|upgraded:|All upgrades|dry-run" /var/log/unattended-upgrades/unattended-upgrades.log | cut -c1-200
'Check. On 2026-10-30 this printed the two 20auto-upgrades lines, two files in the log directory (unattended-upgrades.log and unattended-upgrades-dpkg.log), and log lines starting "Starting unattended upgrades script" and "Allowed origins are:", the list being the release, its security pocket and the two ESM pockets. One run also listed "Packages that will be upgraded: zlib1g" and a line saying the dry-run option was given and nothing was done; an image pulled on another day may list other packages or none. The container has no running init system, so the daily timer does not run: this teaches the configuration and the log, not the schedule.
Undo. --rm removed the container. podman rmi docker.io/library/ubuntu:24.04 frees the image.
The script below reads an unattended-upgrades.log and prints which of the four conditions it finds, with an exit code a monitor can use. It is read-only: it sends nothing. It looks only at the newest run (from the last "Starting unattended upgrades script" line), because one file can hold many runs and an old error is not today's. It treats the age of that run as the "stopped updating" test.
#!/usr/bin/env bash
# uu-triage.sh - read an unattended-upgrades log and say which of four alerts it would raise.
# Usage: ./uu-triage.sh LOGFILE [MAX_AGE_HOURS] (MAX_AGE_HOURS defaults to 36)
# Environment: TRIAGE_NOW epoch seconds to use instead of the clock (for practice and tests)
# REBOOT_FLAG path to the reboot-required file (default /var/run/reboot-required)
# Read-only: it reads the log and the flag file and prints. It sends nothing and changes nothing.
# Exit code: 0 nothing to alert on, 1 a warning, 2 critical, 64 wrong usage.
set -u
[ "$#" -ge 1 ] || { echo "usage: uu-triage.sh LOGFILE [MAX_AGE_HOURS]" >&2; exit 64; }
log=$1
max_h=${2:-36}
flag=${REBOOT_FLAG:-/var/run/reboot-required}
[ -r "$log" ] || { echo "CRITICAL cannot read $log"; exit 2; }
now=${TRIAGE_NOW:-$(date +%s)}
status=0
raise() { [ "$2" -gt "$status" ] && status=$2; echo "$1 $3"; }
# Only the newest run counts: everything from its "Starting" line to the end of the file.
run=$(awk '/Starting unattended upgrades script/ { buf = "" } { buf = buf $0 "\n" } END { printf "%s", buf }' "$log")
started=$(printf '%s' "$run" | grep -m1 -F "Starting unattended upgrades script" | cut -c1-19)
# 4. A host that stopped updating: no run at all, or the newest run is too old.
if [ -z "$started" ]; then
raise CRITICAL 2 "stale: no run recorded in $log"
else
last=$(date -d "$started" +%s) || { echo "CRITICAL cannot read the time '$started'"; exit 2; }
age_h=$(( (now - last) / 3600 ))
[ "$age_h" -gt "$max_h" ] && raise CRITICAL 2 "stale: the newest run began ${age_h} hours ago (limit ${max_h})"
fi
# 3. An update that failed: any ERROR line in the newest run.
errors=$(printf '%s' "$run" | grep -c ' ERROR ')
[ "$errors" -gt 0 ] && raise CRITICAL 2 "failed: ${errors} ERROR line(s) in the newest run"
# 2. A reboot is needed: the flag file exists, or the tool said it was rebooting.
if [ -e "$flag" ] || printf '%s' "$run" | grep -q 'reboot-required, rebooting'; then
raise WARNING 1 "reboot: this host needs a reboot to finish an update"
fi
# 1. Something was installed.
pkgs=$(printf '%s' "$run" | grep -m1 'Packages that will be upgraded:' | sed 's/.*upgraded: *//')
if [ -n "$pkgs" ]; then
if printf '%s' "$run" | grep -q -F -- '--dry-run given'; then
raise NOTICE 0 "dry run, nothing installed; would apply: $pkgs"
else
raise NOTICE 0 "applied: $pkgs"
fi
fi
[ "$status" -eq 0 ] && [ -z "$pkgs" ] && echo "OK nothing to report"
exit "$status"Save it as uu-triage.sh. It needs GNU date (date -d), so run it in the container (Linux) rather than on a Mac. Make a folder with five small logs, written to match the format you saw in Exercise 1:
mkdir -p triage-lab && cd triage-lab
cp ../uu-triage.sh .
printf '%s\n' \
'2026-10-29 06:19:10,100 INFO Starting unattended upgrades script' \
'2026-10-29 06:19:12,000 INFO No packages found that can be upgraded unattended and no pending auto-removals' > ok.log
printf '%s\n' \
'2026-10-29 06:19:10,100 INFO Starting unattended upgrades script' \
'2026-10-29 06:19:12,000 INFO Packages that will be upgraded: libc6 python3-jinja2' \
'2026-10-29 06:19:30,000 INFO All upgrades installed' > applied.log
printf '%s\n' \
'2026-10-29 06:19:10,100 INFO Starting unattended upgrades script' \
'2026-10-29 06:19:12,000 INFO Packages that will be upgraded: libc6' \
'2026-10-29 06:19:30,000 INFO All upgrades installed' \
'2026-10-29 06:19:31,000 WARNING Found /var/run/reboot-required, rebooting' > reboot.log
printf '%s\n' \
'2026-10-28 06:19:10,100 INFO Starting unattended upgrades script' \
'2026-10-28 06:19:30,000 ERROR an old failure from a previous run' \
'2026-10-29 06:19:10,100 INFO Starting unattended upgrades script' \
'2026-10-29 06:19:12,000 INFO Packages that will be upgraded: openssl' \
'2026-10-29 06:19:30,000 ERROR Installing the upgrades failed!' > failed.log
printf '%s\n' '2026-10-29 06:19:10,100 DEBUG nothing useful' > empty.logThe failed.log text is made up to show the shape of an ERROR line; I did not collect real failure messages, so a real failure may read differently and your monitor should key on the level, as this script does, not on my wording.
Predict. Set the clock to noon on the day of the logs. For each of the five files, which line will the script print, and what exit code? Then set the clock to four days later and run ok.log again.
Do. Use TZ=UTC so the log's timestamps and the clock agree, and point REBOOT_FLAG at a path that does not exist so your own machine's flag cannot leak in:
export TZ=UTC REBOOT_FLAG=/nonexistent
NOW=$(date -d '2026-10-29 12:00:00' +%s)
for f in ok applied reboot failed empty; do
echo "== $f"; TRIAGE_NOW=$NOW bash uu-triage.sh $f.log; echo "exit=$?"
done
echo "== stale"; TRIAGE_NOW=$(date -d '2026-11-02 12:00:00' +%s) bash uu-triage.sh ok.log; echo "exit=$?"Check. On 2026-10-30 the script printed, in order: OK nothing to report (exit 0); NOTICE applied: libc6 python3-jinja2 (0); WARNING reboot: ... then NOTICE applied: libc6 (1); CRITICAL failed: 1 ERROR line(s) in the newest run then NOTICE applied: openssl (2; the old error from the day before is not counted); CRITICAL stale: no run recorded in empty.log (2); and for the clock four days on, CRITICAL stale: the newest run began 101 hours ago (limit 36) (2). Now run it on the real log from Exercise 1 inside the container, and again after touch /var/run/reboot-required: the first run printed a dry-run notice and exit 0, the second added the reboot warning and exit 1.
Notice what the script cannot tell you. It cannot see a host whose log was never written, a host that is off, or a log file someone deleted. That is why the check belongs on a machine other than the one it checks.
Undo. --rm and cd .. && rm -r triage-lab.
For a fleet of three machines (one web server, one database, one connector), write a table: for each of the four conditions, who is told, by what route, how fast, and what they do. Then answer: what tells you the notifier has stopped? Compare with what Patch Gremlin's README offers.
- A host's updater logs nothing for a week and sends no message. Is it healthy? Which of the four alerts would tell you, and why does a tool that reports only events miss it?
- You add a 52 file that lists only the
-securityorigin. Does the host now take updates from fewer places? How would you check? - A kernel fix was installed at 06:00. At 06:05 the host is running the old kernel. Why? What decides when it changes?
- You add the vendor's own apt repository for an application and the application is not updated automatically. Is that a bug?
- A Patch Gremlin message says "3 packages updated". Does that tell you the service on the host is working?
Answers
- Not necessarily; unknown. The fourth alert (stale), checking the age of the last run. A tool that reports events only has nothing to report when nothing happens.
- No: a list option adds; the stock origins stay. Run
apt-config dump | grep Allowed-Origins, or read the "Allowed origins are:" line of the tool's log.#clearon the first line replaces. - The new kernel is installed but only loaded at the next boot. The reboot policy:
Automatic-Rebootand its time, or a person. - No, it is the designed behavior (Ubuntu's page). Add the origin if you want it, or give that application its own update job and its own alert.
- No. It says the package manager did its job. Check the service.
- In the container, write a
52file that lists only the-securityorigin, runapt-config dump | grep Allowed-Origins, then put#clear Unattended-Upgrade::Allowed-Origins;on its first line and run it again. Read the difference. - Add a check to the script for the age of the reboot-required flag: warn after one day, critical after three. Where does the host get its modification time?
- Change
MAX_AGE_HOURSto 12 and say what that does to a weekly schedule. Then say what the right limit is for a host that updates weekly. - If you use Patch Gremlin or another notifier, find where it records the last-run time and write the one-line stale check for it.
Vendor pages were read on 2026-10-30 (read-only fetches). The Debian wiki page shows its own last-modified date; the others carry none in the text I read. I took only the statements listed here from each page.
| Claim | Source | Status |
|---|---|---|
Ubuntu applies security updates automatically through unattended-upgrades, installed by default; it does the equivalent of apt update and apt upgrade; runs once a day by default; files 50unattended-upgrades (behavior), 20auto-upgrades (on/off, in days), logs in /var/log/unattended-upgrades; timers apt-daily.timer and apt-daily-upgrade.timer with a random delay; Persistent=false stops catch-up |
Ubuntu Server: automatic updates | Read, 2026-10-30 |
Default Allowed-Origins (release, security, ESM pockets; -updates commented out); another repository is not considered unless its Origin is added; Package-Blacklist as Python regular expressions, anchored as if by ^; blocking can block dependents |
Same page | Read, 2026-10-30; default origins and 20auto-upgrades also observed in the container |
Mail and MailReport (always, only-on-error, on-change; default on-change); needs a mail client; example subject and body |
Same page | Read, 2026-10-30; mail not tried |
Automatic-Reboot (default false), Automatic-Reboot-WithUsers (default true), Automatic-Reboot-Time (default now, hh:mm in the local time zone, passed to shutdown(8)); the log excerpt "Found /var/run/reboot-required, rebooting" |
Same page | Read, 2026-10-30; no real reboot run |
needrestart called after upgrades; restarts services automatically by default from Ubuntu 24.04 LTS, with a built-in exception list; $nrconf{restart} and $nrconf{override_rc} under /etc/needrestart/conf.d/
|
Same page | Read, 2026-10-30; not run |
| When to consider disabling it (recreated systems, manual steps, risk); not a replacement for fleet management, Landscape named | Same page | Read, 2026-10-30 |
Logs: all operations to unattended-upgrades.log, dpkg output to unattended-upgrades-dpkg.log; the tool is the backend for APT::Periodic::Unattended-Upgrade, run by apt-daily-upgrade.service or cron; options --dry-run, --debug
|
unattended-upgrade(8) (unattended-upgrades 2.13, source dated 2026-07-06) |
Read, 2026-10-30; the two files seen in the container (2.9.1) |
Debian: usually installed with conservative settings but may be absent or disabled; check with dpkg-reconfigure; use a 52unattended-upgrades-local copy; Origins-Pattern; check /var/log/dpkg.log and the log directory; apt-listchanges; unattended-upgrade -d; timers and RandomizedDelaySec guidance; laptops skip runs on battery without notice, OnlyOnACPower; needrestart
|
Debian Wiki: PeriodicUpdates (redirects to PeriodicUpdates; last modified 2026-07-10) | Read, 2026-10-30; not run on Debian |
dnf-automatic: apply_updates default False, upgrade_type default or security, reboot never/when-changed/when-needed, reboot_command default shutdown -r +5 ..., timer units -notifyonly, -download, -install
|
DNF Automatic | Read, 2026-10-30; not run |
A 52 file adds to Allowed-Origins rather than replacing it; #clear replaces it |
Author's measurement: apt-config dump in an Ubuntu 24.04.5 container, 2026-10-30 |
Run once; not read on a page (apt.conf(5) not read) |
| Dry run in a fresh container: two log files, "Allowed origins are:", dry-run line | Author's run, 2026-10-30, as in Exercise 1 | Run once per claim |
uu-triage.sh output for the five logs, the stale case, and the real dry-run log |
Author's run, 2026-10-30; tests/test_patch_management_lesson.py runs the script from this page |
Run |
Patch Gremlin: purpose, supported systems and destinations, ExecStartPost hook, heartbeat default off, health check exit codes, Prometheus metric names, state file, journalctl -t patch-gremlin, secrets modes, license line, 2.0 notice, the two descriptions of the trigger |
Patch Gremlin README | README read 2026-10-30; code not read; nothing installed or run |
| Which of the four alerts Patch Gremlin raises for a failed update and for a pending reboot | README | Not stated in the part I read |
| This project's update, baseline and harden order, the 52 file, the weekly connector cron, reboot times, the 60-minute validation, the edge slots |
modules/twingate-bootstrap/cloud-init.yaml.tftpl, stacks/10-access/variables.tf, modules/edge-host/reboot-slots.json, Droplet hardening
|
Read in the repository; tests/test_update_order_pinned.py pins the first-hour order in the template |
| Staging stack planned, not built | Issue #310, Roadmap | Read 2026-10-30 |
journalctl -u apt-daily-upgrade.service, systemctl list-timers, apt install package=version, apt-mark hold, log rotation, "apt has no transaction undo", the stale-metric expression, "weeks" of exposure |
Author's general knowledge | est.; not run, not read on a page |
Next: Backups, recovery and patching, then the Reading order.
Where you are: Home > Track 1: Foundations > Patch management and automatic security updates
Previous: First hour on a new system / Next: Red and blue, and choosing your lab OS
In this track: Track 1: Foundations, 37 pages
- OS and Linux, git and the terminal: Git from zero / The terminal and your first script / Linux for enterprise beginners / First hour on a new system / Patch management and automatic security updates / Red and blue, and choosing your lab OS / Virtual machines and hypervisors: desktop to datacenter / Device firmware security: Intel ME, AMD PSP, MINIX and kernel design / Virtual machines, containers and cloud service models / Kubernetes for beginners / Windows, Active Directory and macOS / Cron, systemd timers and init scripts versus systemd
- Networking: How a web request works / DNS, Cloudflare and hosting your own website / Subnets, NAT and firewalls by hand / Behind a CDN: real client IPs and OSI basics / IPv6 adoption and handling it safely
- Identity and secrets: APIs and tokens from zero / Hashing, encryption and keys / Secrets from day one / Password managers and passkeys / Disk and file encryption: VeraCrypt, TrueCrypt, BitLocker, FileVault, LUKS / Enterprise identity: SSO, SAML and OIDC / Hardware security keys: YubiKey, Nitrokey, PIV
- Infrastructure as code: Data formats: JSON, YAML, HCL / OpenTofu from zero / OpenTofu plans, state, drift and recovery / Ansible: configuration management versus provisioning / Native IaC versus open standards / Spacelift from zero / Build and destroy: one stack per service / Environments and promotion
- Web hosting: LAMP and LEMP from zero / WordPress and PHP security, WAFs and static sites / Static against dynamic: Hugo, WordPress and recon / WordPress at scale: CDN, WAF, caching and CI / Autoscaling and elasticity
Related pages:
Source for these pages lives in docs/wiki/ and is published by the Wiki workflow on every merge to main. Corrections are welcome as pull requests; edits made in the wiki editor are overwritten on the next publish. Apache-2.0 licensed, see LICENSE and NOTICE in the repository.
Support this project: support.chiefgyk3d.com, or see Home.
Home | Contents | Reading order | Why this exists | Give feedback
Core path: read in this order (21)
- Start here if you are new
- What is the cloud
- Networking basics
- Identity, secrets, zero trust
- Infrastructure as code
- Git and pull requests
- Containers and Docker (optional)
- How money works
- Enterprise habits
- Architecture overview
- Why ZTNA and not a VPN
- Twingate
- DigitalOcean network and firewalls
- Droplet hardening
- OpenTofu, stacks and modules
- Spacelift
- Doppler and secrets
- CI, tests and the cost gate
- Threat model
- Lessons learned
- Roadmap
Track 1: Foundations (37)
OS and Linux, git and the terminal (12)
- Git from zero · The terminal and your first script · Linux for enterprise beginners (est. steps) · First hour on a new system
- Patch management and automatic security updates · Red and blue, and choosing your lab OS · Virtual machines and hypervisors: desktop to datacenter (draft)
- Device firmware security: Intel ME, AMD PSP, MINIX and kernel design (draft) · Virtual machines, containers and cloud service models · Kubernetes for beginners (draft)
- Windows, Active Directory and macOS · Cron, systemd timers and init scripts versus systemd (draft)
Networking (5)
Identity and secrets (7)
- APIs and tokens from zero · Hashing, encryption and keys · Secrets from day one · Password managers and passkeys (draft)
- Disk and file encryption: VeraCrypt, TrueCrypt, BitLocker, FileVault, LUKS (draft) · Enterprise identity: SSO, SAML and OIDC
- Hardware security keys: YubiKey, Nitrokey, PIV (draft)
Infrastructure as code (8)
- Data formats: JSON, YAML, HCL · OpenTofu from zero · OpenTofu plans, state, drift and recovery · Ansible: configuration management versus provisioning (draft)
- Native IaC versus open standards (draft) · Spacelift from zero · Build and destroy: one stack per service · Environments and promotion (unbuilt)
Web hosting (5)
Security tools and threat modeling (8)
- Threats in plain words
- Security tools: Nmap, OpenVAS, Burp, Metasploit
- Threat frameworks: STRIDE, ATT&CK, ATLAS
- Email security
- Threat model exercise
- Threat Dragon exercise
Digital forensics (2)
Track 2: Defend and detect (38)
Network and host defense (9)
- Packet filters compared · IDS and IPS: Snort and Suricata · File integrity and alerting · Rate limiting at every layer
- Plan for failure: blast radius and assuming breach · Attack surface: list, shrink, measure (draft) · Attack surface map: layer by layer (draft)
- The DMZ and hardening an internet-facing host (draft) · DMZ hardening checklist (draft)
Deception and honeypots (11)
- Deception and honeypots: start here · What deception is (est. steps) · Deception safety and law (est. steps) · Plant a canary token · Honeyfiles on a host you own
- Deception alerts in the SIEM · Design a honeynet · T-Pot at home (est. steps) · Anatomy of a cloud honeypot (est. steps) · Cloud Cowrie for a weekend (est. steps)
- Read what arrived: Cowrie logs
Detection and monitoring (14)
- A small example: the enterprise landscape · Logs, SIEM and detection basics · Detection, Wazuh and the landscape (unbuilt) · Wazuh SIEM and data pipelines (est. steps)
- Detecting Tor, I2P and VPN on a network (draft) · Cloud telemetry: what and why (draft) · Multiple sites and identity (draft)
- Telemetry crosses the trust boundary (draft) · Reading a dashboard as an operator (draft) · Grafana basics from a blank page (est. steps) · Grafana dashboards
- Lab: synthetic multi-site monitoring stack · Build your own mini SOC (draft) · Stage 2 home-side runbook (unbuilt)
Digital forensics (3)
- DFIR toolkits and live triage (draft) · Digital forensics foundations (draft) · Network, log and timeline forensics (draft)
Device and credential security (1)
Track 3: Privacy, anonymity and cryptocurrency (20)
Cryptography and privacy (4)
- Cryptography fundamentals (draft) · Privacy, security and anonymity (draft) · Privacy versus security (draft)
- Hardware security modules and building your own with pico2hsm (draft)
Tor, I2P, VPN and browsing (9)
- Tor, I2P and the darknet without the myths (draft) · How a VPN works (draft) · How Tor works in depth (draft) · How I2P works (draft)
- The browsing setups compared (draft) · OPSEC for Tor and I2P (draft) · Hands-on Whonix and Qubes OS (draft) · The darknet landscape: markets, OPSEC and layering (draft)
- Lab: publish a web server as an onion service and an I2P site (draft)
Cryptocurrency (6)
- Blockchain fundamentals (draft) · How Bitcoin works (draft) · How Monero works (draft) · Wallets and key custody (draft)
- Paper wallets and offline key generation (draft) · Running Monero properly (draft)
Radio and off-grid (1)
- Radio and off-grid communication (draft)
Track 4: GRC and law (9)
- The CIA triad (draft)
GRC, privacy law and data governance (4)
- GRC in plain terms (draft) · US privacy law and the patchwork (draft) · Privacy law around the world (draft) · Cross-tenant data flows, governance, retention and DLP
Continuity, backups and recovery (4)
- Business continuity and disaster recovery (draft) · Backups, recovery and patching · Backups deep dive (draft) · RAID: continuity, not a backup (draft)
Track 5: Build and operate (32)
Stage guides and design (12)
- Fork, private stacks, the pipeline · Labs: break it on purpose · Build it: zero accounts to stage 1 · Variables, tfvars and secrets · Teardown checklist
- Self-hosting: when and why, with SearXNG (draft) · Twingate service accounts in CAH · Zero trust at home: two connectors and failover
- Why not private droplets (yet) (draft) · Why I picked these tools · From commit to pipeline · Trace one change end to end
Other clouds (10)
- Why the clouds differ (draft) · AWS: account preparation (draft) · AWS: the same four pieces (draft) · AWS: where the money goes (draft)
- Azure: account preparation (draft) · Azure: the same four pieces (draft) · Azure: where the money goes (draft) · GCP: account preparation (draft)
- GCP: the same four pieces (draft) · GCP: where the money goes (draft)