Skip to content

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.

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_CLEAR variable).

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.

Save as /usr/local/bin/notifly-apt-updates:

#!/usr/bin/env bash
set -eu
set -a; source /etc/notifly.env; set +a
# Whether to send a calm "all up to date" when there is nothing to update
NOTIFY_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)
fi
TOTAL="${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 install
UU_COUNT=0
if 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." 3
elif [ "$NOTIFY_ALL_CLEAR" = "1" ]; then
# ✅ Nothing to update
/usr/local/bin/notifly-send \
"✅ Всё актуально — $HOST" \
"Все пакеты обновлены, обновлений безопасности нет." 2
fi

Make it executable:

Окно терминала
sudo chmod +x /usr/local/bin/notifly-apt-updates
Окно терминала
sudo crontab -e

Once a week, on Monday at 9 a.m.:

0 9 * * 1 /usr/local/bin/notifly-apt-updates >/dev/null 2>&1

If 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>&1

/etc/systemd/system/notifly-apt.service:

[Unit]
Description=Notifly apt updates digest
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notifly-apt-updates

/etc/systemd/system/notifly-apt.timer:

[Unit]
Description=Weekly notifly apt updates digest
[Timer]
OnCalendar=Mon 09:00
Persistent=true
[Install]
WantedBy=timers.target

Activate it:

Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now notifly-apt.timer
sudo systemctl list-timers notifly-apt.timer

Persistent=true catches up a missed run if the server was powered off on Monday morning.

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 bash
set -eu
set -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 none
TOTAL=$(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." 3
elif [ "$NOTIFY_ALL_CLEAR" = "1" ]; then
/usr/local/bin/notifly-send \
"✅ Всё актуально — $HOST" \
"Все пакеты обновлены, обновлений безопасности нет." 2
fi

The 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.

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-Windows-Updates.ps1
. C:\scripts\Notifly.ps1
# Install the module (once, as administrator):
# Install-Module PSWindowsUpdate -Force
Import-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 Highest
Register-ScheduledTask -TaskName "Notifly Windows Updates" `
-Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: дайджест обновлений"
  • 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.
  • Attach the full output of apt list --upgradable via the extras field 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.