Mises à jour automatiques de serveurs Debian avec notification ntfy
Un script, un timer systemd et ntfy pour mettre à jour automatiquement des serveurs Debian locaux et des VPS, sans fenêtre bloquante needrestart ni redémarrage automatique.
Objectif
Mettre à jour automatiquement un parc de serveurs Debian (locaux et VPS distants) et recevoir une notification ntfy uniquement quand des mises à jour ont été effectuées, ou en cas d’échec.
Contraintes retenues :
- aucun redémarrage automatique ;
- aucune notification quotidienne « rien à faire » ;
- chaque serveur se met à jour seul (pas de dépendance à une machine centrale) ;
- l’origine de la notification doit être identifiable (nom d’hôte et IP).
Le problème : la fenêtre needrestart
Lors d’une mise à jour qui change le noyau, needrestart ouvre une fenêtre « Pending kernel upgrade » avec un seul bouton OK. Il faut appuyer sur Entrée dans le terminal, ce qui bloque toute mise à jour automatisée.
Solutions possibles :
1
2
# à la volée
sudo NEEDRESTART_MODE=a apt upgrade -y
ou de façon permanente dans /etc/needrestart/needrestart.conf :
1
2
$nrconf{restart} = 'a'; # redémarre automatiquement les services concernés
$nrconf{kernelhints} = -1; # message texte, sans boîte de dialogue
Le script ci-dessous exporte DEBIAN_FRONTEND=noninteractive et NEEDRESTART_MODE=a, ce qui supprime toute interaction.
Un nouveau noyau n’est actif qu’après un
reboot. Le script ne redémarre rien, mais la notification signale quand un redémarrage est nécessaire.
Principe
Trois éléments identiques sur chaque serveur :
| Fichier | Rôle |
|---|---|
/usr/local/sbin/maj-auto |
script de mise à jour et de notification |
/etc/maj-auto.env |
URL ntfy (topic inclus), token, email |
maj-auto.service + maj-auto.timer |
exécution quotidienne par systemd |
Configuration ntfy : ntfy.env
1
2
3
NTFY_URL="https://ntfy.exemple.org/mon_topic"
NTFY_TOKEN="tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
NTFY_EMAIL="ntfy@exemple.org"
Le fichier est installé en 0600 (propriétaire root) sur chaque serveur, car il contient le token.
Le script maj-auto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/bin/bash
set -uo pipefail
source /etc/maj-auto.env
HOST=$(hostname -s)
IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')
[ -z "$IP" ] && IP=$(ip -6 route get 2606:4700:4700::1111 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')
ID="$HOST (${IP:-IP inconnue})"
TS=$(tailscale ip -4 2>/dev/null | head -1)
[ -n "$TS" ] && ID+=" [ts $TS]"
export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a NEEDRESTART_SUSPEND=1
notify() { # titre, priorité, tags, message, [email=1]
local h=(-H "Title: $1" -H "Priority: $2" -H "Tags: $3")
[ -n "${NTFY_TOKEN:-}" ] && h+=(-H "Authorization: Bearer $NTFY_TOKEN")
[ "${5:-0}" = 1 ] && [ -n "${NTFY_EMAIL:-}" ] && h+=(-H "Email: $NTFY_EMAIL")
curl -fsS -m 20 "${h[@]}" -d "$4" "$NTFY_URL" >/dev/null
}
MODE=${1:-}
if [ "$MODE" = "--test-echec" ]; then
notify "$ID : TEST échec mise à jour" high warning \
"Test d'échec (aucune mise à jour réelle)"$'\n'"Voir : journalctl -u maj-auto" 1
exit 0
fi
if [ "$MODE" = "--test" ]; then
LISTE="paquet-exemple-1 paquet-exemple-2 linux-image-amd64 "
PREFIXE="TEST : "
else
PREFIXE=""
APT=(apt-get -y -q -o DPkg::Lock::Timeout=300
-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold)
if ! "${APT[@]}" update; then
notify "$ID : échec apt update" high warning \
"apt-get update a échoué. Voir : journalctl -u maj-auto" 1
exit 1
fi
LISTE=$(apt list --upgradable 2>/dev/null | tail -n +2 | cut -d/ -f1 | tr '\n' ' ')
[ -z "${LISTE// }" ] && { echo "Rien à mettre à jour"; exit 0; }
if ! "${APT[@]}" full-upgrade; then
notify "$ID : échec mise à jour" high warning \
"Paquets prévus : $LISTE"$'\n'"Voir : journalctl -u maj-auto" 1
exit 1
fi
"${APT[@]}" autoremove --purge
fi
MSG="${PREFIXE}$(wc -w <<<"$LISTE") paquet(s) mis à jour : $LISTE"
TAGS="package,white_check_mark"
if [ -f /var/run/reboot-required ]; then
MSG+=$'\n'"⚠ Redémarrage nécessaire (noyau actif : $(uname -r))"
fi
notify "$ID : ${PREFIXE}mises à jour effectuées" default "$TAGS" "$MSG"
Points notables :
--force-confoldet--force-confdefévitent les questions sur les fichiers de configuration modifiés ;DPkg::Lock::Timeout=300attend la libération du verrou dpkg si un autre processus apt tourne ;- le script s’arrête silencieusement s’il n’y a rien à mettre à jour ;
- l’IP est celle de la route par défaut (IPv4, sinon IPv6 pour les VPS IPv6 only), complétée par l’IP Tailscale si elle existe ;
- l’email ntfy n’est joint qu’aux échecs.
Service et timer systemd
1
2
3
4
5
6
7
8
[Unit]
Description=Mise à jour automatique + notification ntfy
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/maj-auto
1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Mise à jour automatique quotidienne
[Timer]
OnCalendar=*-*-* 04:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
RandomizedDelaySec décale les serveurs entre eux, et Persistent=true rattrape une exécution manquée si la machine était éteinte à l’heure prévue.
Installation sur un serveur
1
2
3
4
5
6
7
8
9
#!/bin/bash
set -e
d=$1
install -m750 "$d/maj-auto" /usr/local/sbin/maj-auto
install -m600 "$d/ntfy.env" /etc/maj-auto.env
install -m644 "$d/maj-auto.service" "$d/maj-auto.timer" /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now maj-auto.timer
systemctl list-timers maj-auto.timer --no-pager
Déploiement sur tous les serveurs
Les fichiers sont empaquetés (tar + base64) et envoyés dans une seule commande SSH. Ici, la connexion passe par un script maison (sshm <alias> exe 'commande'), mais un simple ssh "$a" "commande" convient aussi.
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# usage : ./deploy-maj-auto.sh alias1 alias2 ...
set -euo pipefail
cd "$(dirname "$0")"
PAYLOAD=$(tar czf - maj-auto maj-auto.service maj-auto.timer ntfy.env install.sh | base64 -w0)
for a in "$@"; do
echo ">>> $a"
sshm "$a" exe "d=\$(mktemp -d) && echo '$PAYLOAD' | base64 -d | tar xz -C \$d \
&& S=\$([ \$(id -u) = 0 ] || echo sudo) && \$S bash \$d/install.sh \$d; rm -rf \$d"
done
Le déploiement est répétable : relancer la commande réinstalle simplement les fichiers, ce qui sert aussi à propager une modification du script.
Le dossier de déploiement contient le token ntfy : gardez-le avec des droits restreints (
chmod 700). Le déploiement supposesudosans mot de passe sur les serveurs non root.
Tests
Sur la machine
Deux options intégrées envoient une notification complète sans exécuter apt :
1
2
sudo /usr/local/sbin/maj-auto --test # simulation « mises à jour effectuées »
sudo /usr/local/sbin/maj-auto --test-echec # simulation d'échec, avec email
Vérification du timer et du journal du vrai passage :
1
2
3
systemctl list-timers maj-auto.timer
sudo systemctl start maj-auto.service
sudo journalctl -u maj-auto -n 30 --no-pager
/etc/maj-auto.envest en0600: un test avecsource /etc/maj-auto.envdoit se faire soussudo, sinon « Permission non accordée ». Le timer, lui, tourne en root et n’est pas concerné.
Depuis le poste ayant sshm
1
2
3
4
# simulation « mises à jour effectuées »
sshm alias exe 'sudo /usr/local/sbin/maj-auto --test'
# simulation d'échec, avec email
sshm alias exe 'sudo /usr/local/sbin/maj-auto --test-echec'
Contenu des notifications
Mises à jour effectuées (priorité normale, icônes 📦 ✅) :
1
2
3
Titre : srv-web (192.168.0.12) [ts 100.64.0.5] : mises à jour effectuées
Corps : 5 paquet(s) mis à jour : libc6 libssl3 openssl curl linux-image-amd64
⚠ Redémarrage nécessaire (noyau actif : 6.12.43+deb13-amd64)
La ligne « Redémarrage nécessaire » n’apparaît que si /var/run/reboot-required existe.
Échec de apt update ou de la mise à jour : priorité haute, icône ⚠️, email envoyé à NTFY_EMAIL, et renvoi vers journalctl -u maj-auto.
Si tout est déjà à jour, rien n’est envoyé.
Points d’attention
- Redémarrage : à faire manuellement, au moment voulu, quand la notification le signale.
- Proxmox / cluster :
full-upgradeconvient aux hôtes PVE, mais mieux vaut éviter de mettre à jour tous les nœuds d’un cluster simultanément ; le délai aléatoire du timer les décale déjà. - Longueur du message : ntfy accepte environ 4 Ko par message. Sur une première exécution avec de nombreux paquets, la liste peut être tronquée côté script si nécessaire.
- Alternative :
unattended-upgradesfait nativement des mises à jour de sécurité seules, mais ne sait pas notifier via ntfy ; il faudrait le coupler à un script de ce type.