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équille
  • AlwaysAutoRebootAtScheduledTimenon 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 :

GPO avant - AUOptions=4

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 :

GPO après - AUOptions=3

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é

Defender - recherche au démarrage

Nombre de jours avant péremption (virus) → 1

Defender - péremption virus

Nombre de jours avant péremption (logiciels espions) → 1

Defender - péremption spywares

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 :

WinRM - règle firewall restreinte au serveur Ansible

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 :

Windows Update GUI - updates 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é.