C’est un chantier auquel je pensais depuis un moment, mais il etait indispensable d’en avoir finalisé d’autres (surtout le vault pour ceux qui ont lu les articles précedent) avant de m’attaquer à celui la; Par contre le dernier reboot de mon hyperviseur en pleine nuit a déclaré la guerre. C’est l’étincelle qui a fait déborder le vase.
L’objectif : mettre en place une chaîne complète pour choisir quand les mises à jour s’installent et quand les machines redémarrent, avec notification par mail. Exactement ce qui est déjà en place pour les serveurs Linux via Ansible.
Ce qui était déjà en place
Sur mon infra perso je dispose déja d’un serveur WSUS interne (HTTPS, ports 8530/8531) vers lequel toutes les machines Windows pointent via GPO. Les mises à jour sont approuvées manuellement, le serveur m’envois un mail lorsqu’il y a des maj en attende de validation — seules les définitions Microsoft Defender partent en approbation automatique.
La béquille en place pour éviter les reboots sauvages consistait à laisser une session ouverte sur l’hyperviseur, combinée à la GPO “Pas de redémarrage automatique avec utilisateurs connectés”. Fragile. Pas scalable. Et si la session est fermée accidentellment, c’est le reboot assuré.
Partie 1 — Corriger la GPO Windows Update
Le vrai problème
Première étape : voir ce que la GPO impose réellement aux machines. Depuis n’importe quel serveur Windows en PowerShell admin :
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" |
Select-Object AUOptions, NoAutoRebootWithLoggedOnUsers, AlwaysAutoRebootAtScheduledTime
Le résultat :
AUOptions = 4→ téléchargement et installation automatiques (ca évitait de devoir lancer l’install des updates manuellement machine par machine mais ca c’était avant)NoAutoRebootWithLoggedOnUsers = 1→ la béquilleAlwaysAutoRebootAtScheduledTime→ non défini — comportement imprévisible selon l’OS
Le coupable c’est AUOptions = 4. Dès qu’une mise à jour est approuvée sur le WSUS, la machine l’installe et peut redémarrer. La valeur 3 change tout : Windows télécharge, notifie, et va mainteant attendre sagement qu’on décide de la suite.
La modification
La GPO concernée est celle qui déploie le serveur WSUS. Elle contient déjà le “Pas de redémarrage automatique avec utilisateurs connectés” — on y ajoute les corrections directement, pas besoin d’en créer une nouvelle.
Avant :
Configuration ordinateur
└── Modèles d'administration
└── Composants Windows
└── Windows Update
| Paramètre | Avant | Après |
|---|---|---|
| Configuration du service Mises à jour automatiques | 4 | 3 |
| Pas de redémarrage automatique avec utilisateurs connectés | Activé | Activé (inchangé) |
| Toujours redémarrer automatiquement à l’heure planifiée | Non défini | Désactivé |
Après :
Résultat : les mises à jour approuvées sur le WSUS sont téléchargées automatiquement, mais n’installent jamais seules. C’est Ansible qui décide quand installer, et l’admin qui décide quand redémarrer.
Partie 2 — Garder Defender à jour malgré tout
Effet de bord immédiat d’AUOptions=3 : les définitions Defender en approbation automatique ne s’installeraient plus non plus. Or les définitions antivirus doivent être fraîches quotidiennement — et elles ne nécessitent jamais de reboot.
Solution : configurer Defender pour gérer ses définitions indépendamment de WSUS, via sa propre GPO.
Configuration ordinateur
└── Modèles d'administration
└── Composants Windows
└── Antivirus Microsoft Defender
└── Mises à jour du renseignement de sécurité
Trois paramètres à activer :
Rechercher la dernière veille de sécurité au démarrage → Activé
Nombre de jours avant péremption (virus) → 1
Nombre de jours avant péremption (logiciels espions) → 1
Defender vérifie et installe ses définitions de manière totalement autonome. Les logs confirment l’installation quotidienne automatique sans aucune intervention.
Partie 3 — Déployer WinRM via GPO
Ansible a besoin de WinRM pour piloter les mises à jour à distance. Une GPO dédiée s’en charge.
Paramètres de la GPO WinRM
Activer le listener :
Configuration ordinateur → Modèles d'administration
→ Composants Windows → Gestion à distance de Windows (WinRM)
→ Service WinRM
→ Autoriser la gestion de serveurs à distance via WinRM
→ Activé — Filtre IPv4 : *
Le * est important — il signifie que WinRM écoute sur toutes les interfaces de la machine. Un filtre sur une IP spécifique restreindrait l’écoute à cette adresse, pas les connexions entrantes.
Règles pare-feu :
Deux règles dans Paramètres de sécurité → Pare-feu Windows avec fonctions avancées → Règles de trafic entrant :
- Port 5985 (HTTP) — règle prédéfinie “Gestion à distance de Windows (HTTP-Entrée)”
- Port 5986 (HTTPS) — règle personnalisée
Pour les deux, l’adresse IP distante est restreinte à l’IP du serveur Ansible uniquement :
En HTTP, la surface d’attaque reste limitée — WinRM n’est accessible que depuis la machine d’administration.
Démarrage automatique du service :
Configuration ordinateur → Paramètres Windows
→ Paramètres de sécurité → Services système
→ Gestion à distance de Windows (WS-Management)
→ Mode de démarrage : Automatique
Vérification post-déploiement
Depuis le serveur Ansible, après gpupdate /force sur les machines :
for host in IP1 IP2 IP3; do
echo -n "$host 5985: "
nc -zv -w2 $host 5985 2>&1 | grep -o 'succeeded\|refused\|timed out'
done
Point de troubleshooting — Résidus de vieilles GPO
Deux machines refusaient obstinément toute connexion NTLM malgré une config WinRM identique aux autres. Les logs de sécurité Windows (Event ID 4625) ont livré le code d’erreur 0x80090302 (SEC_E_UNSUPPORTED_FUNCTION) — et surtout : le nom de compte était vide dans le log. Le handshake NTLM échouait avant même l’authentification.
Après investigation, les coupables étaient des clés de registre résiduelles dans HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0 :
restrictreceivingntlmtraffic = 2 (Deny all)
restrictsendingntlmtraffic = 2 (Deny all)
Ces clés viennent de vieilles GPO de durcissement NTLM qui n’existaient plus mais dont les valeurs étaient restées dans le registre. La valeur 2 bloque tout trafic NTLM entrant et sortant.
Suppression des clés résiduelles + reboot — les machines répondent immédiatement.
Leçon retenue : quand on supprime une GPO de durcissement, toujours déployer une contre-GPO qui remet les paramètres à leur valeur par défaut, et vérifier qu’elle s’est bien appliquée sur toutes les machines avant de supprimer l’originale. Sinon les valeurs restent en place indéfiniment dans le registre.
Partie 4 — Le playbook Ansible
Inventaire WinRM
Un fichier d’inventaire dédié est créé pour les machines Windows, séparé des inventaires existants :
[domaine1]
SERVEUR-A ansible_host=serveur-a.domaine1.local
SERVEUR-B ansible_host=serveur-b.domaine1.local
[domaine1:vars]
ansible_user=domaine1\ansible_svc
ansible_password={{ vault_ansible_password_domaine1 }}
ansible_connection=winrm
ansible_winrm_transport=ntlm
ansible_winrm_port=5985
ansible_winrm_scheme=http
ansible_winrm_server_cert_validation=ignore
[domaine2]
SERVEUR-C ansible_host=serveur-c.domaine2.local
SERVEUR-D ansible_host=serveur-d.domaine2.local
[domaine2:vars]
ansible_user=domaine2\ansible_svc
ansible_password={{ vault_ansible_password_domaine2 }}
ansible_connection=winrm
ansible_winrm_transport=ntlm
ansible_winrm_port=5985
ansible_winrm_scheme=http
ansible_winrm_server_cert_validation=ignore
[windows_servers:children]
domaine1
domaine2
Les mots de passe des comptes de service sont dans le vault Ansible existant.
windows-updates.yml
Le playbook fonctionne en deux phases : installation sur toutes les machines en parallèle, puis envoi d’un unique mail récapitulatif en fin de run.
---
- name: Windows Updates - Installation sans reboot
hosts: windows_servers
gather_facts: true
tasks:
- name: Rechercher les updates disponibles
ansible.windows.win_updates:
state: searched
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
- Updates
register: updates_found
- name: Installer les updates sans rebooter
ansible.windows.win_updates:
state: installed
reboot: false
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
- Updates
register: install_result
when: updates_found.found_update_count > 0
- name: Stocker le résultat
set_fact:
rapport_machine:
host: "{{ inventory_hostname }}"
updates_count: "{{ install_result.installed_update_count | default(0) }}"
reboot_required: "{{ install_result.reboot_required | default(false) }}"
updates_list: "{{ install_result.updates.values() | map(attribute='title') | list if install_result.updates is defined else [] }}"
status: "{{ 'REBOOT REQUIS' if install_result.reboot_required | default(false) else ('OK' if updates_found.found_update_count > 0 else 'À JOUR') }}"
- name: Envoi du rapport récapitulatif
hosts: localhost
gather_facts: true
tasks:
- name: Envoyer le mail récapitulatif
community.general.mail:
host: "smtp.domaine.local"
port: 25
from: "ansible@serveur-ansible.domaine.local"
to: "admin@domaine.local"
subject: "[WINDOWS UPDATE] {{ update_count }} update(s) — {{ reboot_count }} reboot(s) requis — {{ ansible_date_time.date }}"
body: "{{ rapport_complet }}"
secure: never
windows-reboot.yml
Quand le mail indique qu’un reboot est requis, on choisit le moment et on lance :
---
- name: Reboot contrôlé Windows
hosts: "{{ target }}"
gather_facts: false
tasks:
- name: Reboot propre avec attente retour
ansible.windows.win_reboot:
reboot_timeout: 600
msg: "Reboot planifié - maintenance Ansible"
- name: Confirmer le retour de la machine
ansible.windows.win_ping:
ansible-playbook /etc/ansible/playbooks/windows-reboot.yml \
-i /etc/ansible/winhosts.ini \
--vault-password-file /etc/ansible/vault/.vault_pass \
-e "@/etc/ansible/vault/network-secrets.yml" \
-e "target=NOM_SERVEUR"
Le résultat en conditions réelles
Premier run sur le parc complet. Un hyperviseur avait deux cumulatives en attente :
Le playbook les a installées sans rebooter, et le mail récapitulatif est arrivé :
[WINDOWS UPDATE] 2 update(s) — 1 reboot(s) requis — 2026-04-21
[HYPERVISEUR-PRA]
Statut : REBOOT REQUIS
Updates : 2 installée(s)
Reboot : ⚠️ REQUIS
Détail :
- 2026-xx Mise à jour cumulative pour Microsoft server
operating system version xxx (KBxxxxxxx)
- 2026-xx Mise à jour cumulative de .NET Framework 3.5,
4.8 et 4.8.1 (KBxxxxxxx)
Commande reboot :
ansible-playbook windows-reboot.yml -e "target=HYPERVISEUR-PRA"
L’hyperviseur a redémarré le lendemain matin, planifié, maîtrisé. Pas à 3h du matin.
Planification
Le playbook est intégré au crontab du serveur Ansible, en tenant compte des contraintes de l’infra (certaines machines démarrent par alarme RTC le matin et s’éteignent après leurs jobs de backup) :
# Windows Updates — lundi et jeudi à 7h10
10 7 * * 1,4 ansible-playbook /etc/ansible/playbooks/windows-updates.yml \
-i /etc/ansible/winhosts.ini \
--vault-password-file /etc/ansible/vault/.vault_pass \
-e "@/etc/ansible/vault/network-secrets.yml" \
>> /home/user/logs/ansible-windows-updates.log 2>&1
Le workflow final
WSUS approuve une mise à jour
↓
Machine télécharge (AUOptions=3) — rien de plus
↓
Ansible lance le playbook (lundi/jeudi 7h10)
Toutes les machines en parallèle, installation sans reboot
↓
Un seul mail récap : statut de chaque machine,
liste des updates, reboot requis ou non
↓
On redémarre quand on veut, machine par machine
La suite
Ce chantier sera complété par le passage en HTTPS avec certificats signés par la PKI interne. Ça permettra de passer de NTLM à Kerberos avec validation complète des certificats — une configuration plus propre, et un meilleur article. L’audit et le nettoyage complet des GPO des deux domaines fera aussi l’objet d’un autre chantier dédié.