Pending Security Updates Notification
Packages with known vulnerabilities that wait months for apt upgrade are an
open door. The problem isn’t that updates are hard to install — it’s that they’re
easy to forget: nobody logs into every server to check apt list --upgradable.
Let the server send a digest once a week itself — and if security updates
specifically appear, remind you more insistently.
What we’ll do
Section titled “What we’ll do”Once a week the script refreshes the package index and counts how many packages can be upgraded in total and how many of them are security updates:
- if there are security updates — it sends a digest with priority 6–7 (normal, with a notification) so you don’t miss it;
- if there are only regular updates — a quiet digest with priority 3;
- if there is nothing to update — it optionally sends a calm “all up to date”
with priority 2 (can be disabled via the
NOTIFY_ALL_CLEARvariable).
We take the number of security packages from /usr/lib/update-notifier/apt-check
(the very counter that shows the MOTD when you log into Ubuntu), and the list —
from apt list --upgradable filtered by the -security repository. As a safety
net we run unattended-upgrades --dry-run to see what exactly would have been
installed automatically.
Script
Section titled “Script”Save as /usr/local/bin/notifly-apt-updates:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
# Whether to send a calm "all up to date" when there is nothing to updateNOTIFY_ALL_CLEAR="${NOTIFY_ALL_CLEAR:-1}"
HOST=$(hostname -s)
# Refresh the package index (without installing)apt-get update -qq 2>/dev/null || true
# apt-check prints "updates;security-updates" to stderr — e.g. "12;4"if [ -x /usr/lib/update-notifier/apt-check ]; then read -r TOTAL SECURITY < <(/usr/lib/update-notifier/apt-check 2>&1 | tr ';' ' ')else # Fallback if update-notifier-common is not installed TOTAL=$(apt list --upgradable 2>/dev/null | grep -c '/' || true) SECURITY=$(apt list --upgradable 2>/dev/null | grep -Ec '\-security' || true)fiTOTAL="${TOTAL:-0}"SECURITY="${SECURITY:-0}"
# List of security packages specifically (first 15 lines for the message body)SEC_LIST=$(apt list --upgradable 2>/dev/null \ | grep -E '\-security' \ | awk -F/ '{print "• " $1}' \ | head -n 15)
# How many packages unattended-upgrades would actually installUU_COUNT=0if command -v unattended-upgrades >/dev/null 2>&1; then UU_COUNT=$(unattended-upgrades --dry-run 2>/dev/null \ | grep -Ec '^\s+' || true)fi
if [ "$SECURITY" -gt 0 ]; then # 🔒 Security updates available — remind more insistently PRIO=7 [ "$SECURITY" -le 2 ] && PRIO=6 /usr/local/bin/notifly-send \ "🔒 $SECURITY security-обновлений — $HOST" \ "Всего к обновлению: $TOTAL. unattended-upgrades поставил бы: $UU_COUNT.$SEC_LIST" "$PRIO"elif [ "$TOTAL" -gt 0 ]; then # 📦 Only regular updates — a quiet digest /usr/local/bin/notifly-send \ "📦 Доступно обновлений: $TOTAL — $HOST" \ "Security-обновлений нет. Можно запланировать обычный apt upgrade." 3elif [ "$NOTIFY_ALL_CLEAR" = "1" ]; then # ✅ Nothing to update /usr/local/bin/notifly-send \ "✅ Всё актуально — $HOST" \ "Все пакеты обновлены, обновлений безопасности нет." 2fiMake it executable:
sudo chmod +x /usr/local/bin/notifly-apt-updatesRun on schedule
Section titled “Run on schedule”Using cron
Section titled “Using cron”sudo crontab -eOnce a week, on Monday at 9 a.m.:
0 9 * * 1 /usr/local/bin/notifly-apt-updates >/dev/null 2>&1If you want to learn about security updates sooner, run it daily — the script
will still stay silent when there is nothing to update (with NOTIFY_ALL_CLEAR=0):
0 9 * * * NOTIFY_ALL_CLEAR=0 /usr/local/bin/notifly-apt-updates >/dev/null 2>&1Using systemd timer (recommended)
Section titled “Using systemd timer (recommended)”/etc/systemd/system/notifly-apt.service:
[Unit]Description=Notifly apt updates digest
[Service]Type=oneshotExecStart=/usr/local/bin/notifly-apt-updates/etc/systemd/system/notifly-apt.timer:
[Unit]Description=Weekly notifly apt updates digest
[Timer]OnCalendar=Mon 09:00Persistent=true
[Install]WantedBy=timers.targetActivate it:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-apt.timersudo systemctl list-timers notifly-apt.timerPersistent=true catches up a missed run if the server was powered off on
Monday morning.
Alternative: RHEL / dnf
Section titled “Alternative: RHEL / dnf”On RHEL, Rocky, AlmaLinux, and Fedora, packages and security updates are counted
via dnf. The dnf-plugin-security plugin (built into RHEL 8+) can filter
security advisories specifically:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
NOTIFY_ALL_CLEAR="${NOTIFY_ALL_CLEAR:-1}"HOST=$(hostname -s)
# check-update returns code 100 if there are updates, 0 if there are noneTOTAL=$(dnf -q check-update 2>/dev/null | grep -Ec '^\S+\s+\S+\s+\S+$' || true)SECURITY=$(dnf -q check-update --security 2>/dev/null | grep -Ec '^\S+\s+\S+\s+\S+$' || true)
# Readable list of security advisories (RHSA/ALSA etc.)SEC_LIST=$(dnf -q updateinfo list security 2>/dev/null \ | awk '{print "• " $1 " " $3}' \ | head -n 15)
TOTAL="${TOTAL:-0}"; SECURITY="${SECURITY:-0}"
if [ "$SECURITY" -gt 0 ]; then PRIO=7 [ "$SECURITY" -le 2 ] && PRIO=6 /usr/local/bin/notifly-send \ "🔒 $SECURITY security-обновлений — $HOST" \ "Всего к обновлению: $TOTAL.$SEC_LIST" "$PRIO"elif [ "$TOTAL" -gt 0 ]; then /usr/local/bin/notifly-send \ "📦 Доступно обновлений: $TOTAL — $HOST" \ "Security-обновлений нет. Можно запланировать обычный dnf upgrade." 3elif [ "$NOTIFY_ALL_CLEAR" = "1" ]; then /usr/local/bin/notifly-send \ "✅ Всё актуально — $HOST" \ "Все пакеты обновлены, обновлений безопасности нет." 2fiThe schedule is the same (OnCalendar=Mon 09:00 or a weekly cron). A quick
one-command check without a script: dnf updateinfo list security shows the
list of advisories, and dnf check-update --security shows the packages themselves.
Windows: PowerShell + Task Scheduler
Section titled “Windows: PowerShell + Task Scheduler”On Windows servers available updates are counted by the
PSWindowsUpdate module.
It uses the common function Send-Notifly
from the sysadmin/index template.
. C:\scripts\Notifly.ps1
# Install the module (once, as administrator):# Install-Module PSWindowsUpdate -ForceImport-Module PSWindowsUpdate
$NotifyAllClear = $true$Host = $env:COMPUTERNAME
# All pending updates$updates = Get-WindowsUpdate -MicrosoftUpdate -ErrorAction SilentlyContinue$total = ($updates | Measure-Object).Count
# Critical / security updates only$security = $updates | Where-Object { $_.Categories -match "Security" -or $_.MsrcSeverity -in @("Critical", "Important")}$secCount = ($security | Measure-Object).Count
# First 15 titles for the message body$secList = ($security | Select-Object -First 15 | ForEach-Object { "• " + $_.Title }) -join "`n"
if ($secCount -gt 0) { $prio = if ($secCount -le 2) { 6 } else { 7 } Send-Notifly -Title "🔒 $secCount security-обновлений — $Host" ` -Message "Всего к обновлению: $total.`n$secList" -Priority $prio} elseif ($total -gt 0) { Send-Notifly -Title "📦 Доступно обновлений: $total — $Host" ` -Message "Security-обновлений нет. Можно запланировать установку." -Priority 3} elseif ($NotifyAllClear) { Send-Notifly -Title "✅ Всё актуально — $Host" ` -Message "Все обновления установлены, критических нет." -Priority 2}Register in Task Scheduler (as administrator, once a week as SYSTEM):
$Action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Windows-Updates.ps1"$Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 09:00$Princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel HighestRegister-ScheduledTask -TaskName "Notifly Windows Updates" ` -Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: дайджест обновлений"Benefits
Section titled “Benefits”- Vulnerable packages don’t linger for months. A reminder arrives once a week, and when security updates appear — with a higher priority so it doesn’t get lost among the quiet digests.
- One channel for the entire server fleet. The title has the hostname, and the body — how many updates in total and which of them are security. You see the whole picture without logging into the machines.
- A sensible noise level. Regular updates arrive quietly (priority 3), and “all up to date” can be turned off entirely — notifications don’t turn into spam.
What to improve next
Section titled “What to improve next”- Attach the full output of
apt list --upgradablevia theextrasfield to see the whole list without logging into the server. - Store state, as in the disk recipe, to avoid repeating the same digest until the list changes.
- Install security updates automatically (
unattended-upgrades) and send Notifly only the fact of installation and the need to reboot (/var/run/reboot-required). - On priorities and notification sound — see the section on sending messages.