Les Parties 1 et 2 racontaient la bascule de ma PKI interne, côté Linux puis côté Microsoft AD CS. Ce troisième volet est un chantier annexe, né d’un changement de comportement de Windows plutôt que d’un projet planifié : depuis avril 2026, tout fichier .rdp non signé numériquement déclenche un avertissement de sécurité agressif — même pour des fichiers que je crée moi-même, sur mon propre réseau, pour me connecter à mes propres machines.

Ce n’est pas un article sur un projet propre et linéaire. C’est le récit d’un chantier qui a marché du premier coup sur la partie serveur, et qui m’a donné du fil à retordre sur la partie la plus simple en apparence : faire tourner un script en tâche de fond de façon fiable.

Le déclencheur : CVE-2026-26151

En avril 2026, Microsoft a durci le comportement de Connexion Bureau à distance (mstsc) face aux fichiers .rdp : un fichier non signé numériquement par un éditeur de confiance déclenche désormais un bandeau rouge “Serveur de publication inconnu”, qui pousse implicitement au clic réflexe — exactement l’inverse de l’effet recherché — et qui, en pratique, coupe certaines redirections (carte à puce notamment) tant qu’on n’a pas validé l’avertissement.

Avertissement “Attention : connexion distante inconnue” sur un fichier .rdp non signé

Sur mon infra perso, où je génère des fichiers .rdp à la volée sur chaque poste plutôt que de les stocker dans un dossier central, ce bandeau est devenu une gêne quotidienne. Plutôt que de l’ignorer par réflexe (ce qui, à terme, désensibilise à l’avertissement réel), j’ai profité du fait que mon AD CS interne était déjà en place pour signer ces fichiers automatiquement.

Le certificat de signature de code

Première étape : un nouveau template de certificat, dédié à la signature de code (xxxxxxxxxxxx-RDP-File-Signing), avec l’EKU Signature du code (1.3.6.1.5.5.7.3.3), un KSP moderne (RSA 3072, SHA-256), et un sujet basé sur le nom DNS de la machine porteuse de la clé.

Onglet Général du template xxxxxxxxxxx-RDP-File-Signing

Extensions : stratégie d’application Signature du code

Chiffrement : KSP, RSA 3072, SHA-256

Le piège CONTEXT_E_ROLENOTFOUND

Premier vrai obstacle, et le plus coûteux en temps : toute tentative d’inscription (autoenrollment comme certreq -enroll -machine manuel) échouait systématiquement avec CONTEXT_E_ROLENOTFOUND (0x8004e00c) — “le rôle spécifié n’a pas été configuré pour l’application”. Aucune trace dans les journaux CAPI2, aucune trace côté autorité de certification. Une erreur silencieuse et sans piste évidente.

J’ai vérifié dans l’ordre : les ACL du template, le fournisseur de stockage de clés, les extensions EKU, le nom du sujet (j’y ai d’ailleurs trouvé une vraie erreur secondaire — le nom principal de l’utilisateur (UPN) était coché dans le format du nom de sujet, inapproprié pour un certificat de machine, corrigé au passage mais qui n’expliquait pas l’erreur principale), les paramètres de compatibilité, l’attestation de clé. Tout était correct.

Onglet Nom du sujet — UPN coché à tort, corrigé au passage mais pas la cause de l’erreur principale

La cause réelle, trouvée après avoir épluché la documentation de bout en bout : j’avais dupliqué ce template à partir du template Microsoft intégré “Signature de code”, qui est — au niveau interne du schéma Active Directory — un template de type Utilisateur. Ce marqueur de type n’est exposé dans aucun onglet de certtmpl.msc : il est hérité silencieusement du template source au moment de la duplication, indépendamment des droits ACL qu’on accorde ensuite à un compte machine. Un template destiné à émettre un certificat pour un compte machine doit impérativement être dupliqué depuis un template déjà de type Ordinateur (par exemple “Ordinateur” ou “Serveur Web”), jamais depuis un template Utilisateur retravaillé après coup, aussi correctement configurés qu’en soient les autres paramètres.

Une fois le template recréé depuis une base Ordinateur puis reconfiguré à l’identique (EKU, KSP, sujet), l’inscription a fonctionné du premier coup.

En cours de route, avant de trouver la vraie cause, j’ai aussi buté sur une erreur de permissions plus classique — utile à documenter parce qu’elle peut faire perdre du temps en la confondant avec le vrai problème :

« Droits d’accès insuffisants » lors de la modification du template — erreur de permissions distincte de CONTEXT_E_ROLENOTFOUND

Architecture retenue : la clé reste centralisée

Sur mon infra, les fichiers .rdp sont créés localement, à la volée, sur chaque poste — pas de dossier central à surveiller. Deux options s’offraient pour signer ces fichiers :

  • Option B (écartée) : dupliquer la clé privée de signature sur chaque poste, pour signer localement. Simple en apparence, mais ça multiplie la surface d’exposition par le nombre de postes, et la compromission d’un seul poste compromet la clé de signature de toute l’infra.
  • Option A (retenue) : garder la clé privée centralisée sur l’autorité de certification (AD-CS), et faire signer à distance via PowerShell Remoting, à la demande, chaque fois qu’un .rdp local doit être signé.

Option A : clé centralisée, signature à la demande via WinRM/JEA — contre Option B : clé dupliquée sur chaque poste

L’endpoint JEA “RdpSigning”

Pour éviter d’ouvrir un accès WinRM administratif complet sur AD-CS, j’ai construit un endpoint JEA (Just Enough Administration) dédié : une session PowerShell restreinte (RestrictedRemoteServer), tournant sous un compte virtuel (RunAsVirtualAccount), qui n’expose qu’une seule fonction (Invoke-RdpSign) et rien d’autre du système.

Ma forêt AD ayant deux domaines, l’accès est scopé aux groupes Domaine1\Admins du domaine et Domaine2\Admins du domaine — via les RoleDefinitions de JEA, qui acceptent nativement plusieurs domaines sans qu’il soit nécessaire de créer un groupe universel cross-domaine (je préfère éviter ce genre de dépendance quand une solution native et cloisonnée par domaine existe).

Trois pièges rencontrés pendant la construction :

  • Structure de module versionnée sans manifeste. Une arborescence Modules\RdpSigningJEA\1.0\... est invisible à la découverte de module par nom si elle ne contient pas de fichier manifeste .psd1. Corrigé en aplatissant le module (suppression du sous-dossier de version).
  • Select-Object -First non supporté en session RestrictedRemoteServer, même à l’intérieur d’une fonction de module importé et de confiance. Contourné avec de l’indexation de tableau classique.
  • $env:TEMP non défini pour un compte virtuel RunAsVirtualAccount (pas de profil utilisateur chargé). Corrigé avec un chemin fixe (C:\ProgramData\RdpSigningJEA\Temp) plutôt que de dépendre d’une variable d’environnement absente.

Un test de bout en bout depuis un poste client a validé l’endpoint : appel Invoke-Command -ConfigurationName RdpSigning, retour Success=True avec le bon empreinte du certificat de signature.

Le watcher côté client

Côté poste client, un script PowerShell basé sur FileSystemWatcher et Register-ObjectEvent surveille le Bureau, détecte tout fichier .rdp créé ou modifié, appelle l’endpoint JEA, et réécrit le fichier signé reçu en retour.

Deux pièges au passage :

  • $using: n’est pas valide dans un scriptblock -Action de Register-ObjectEvent (cette syntaxe n’est valide que dans Invoke-Command ou Start-Job). Corrigé en passant les valeurs nécessaires via -MessageData, récupérées ensuite avec $Event.MessageData.
  • Course critique avec mstsc. Le client Bureau à distance garde parfois le fichier .rdp ouvert en écriture un court instant après sa création. Avec un délai fixe de 800 ms avant lecture, le watcher lisait parfois le fichier trop tôt, produisant un fichier corrompu — une propriété de fin de fichier atterrissant après le bloc de signature :

« Ce fichier RDP est endommagé » — conséquence de la course entre mstsc et une lecture trop précoce du watcher

Corrigé en remplaçant le délai fixe par une boucle d’attente qui tente une ouverture exclusive du fichier (FileShare::None) jusqu’à ce que mstsc l’ait réellement relâché, avant de le lire et de le signer.

De la création du .rdp au bandeau “éditeur vérifié”

Rendre le watcher persistant : le chantier qui a mal tourné

C’est la partie la moins glorieuse de cet article, et probablement la plus utile pour quelqu’un qui tenterait la même chose : le watcher, une fois validé, ne tournait qu’en PowerShell interactif — sans persistance. Une fermeture de fenêtre ou un redémarrage du poste, et plus rien ne surveille le Bureau.

L’intention de départ était simple : une tâche planifiée déployée par GPO, dans Configuration utilisateur → Préférences → Tâches planifiées, déclenchée à l’ouverture de session, exécutée dans le contexte de l’utilisateur connecté (%LogonUser%, nécessaire pour l’authentification Kerberos face à l’endpoint JEA), ciblée par un filtrage par groupe de sécurité (Item-Level Targeting) sur Admins du domaine plutôt que par OU — les comptes admin n’étant pas isolés dans une OU dédiée.

Configuration de la tâche planifiée GPP — Général Déclencheur : à l’ouverture de session Action : lancement du watcher via PowerShell caché Paramètres : ne pas démarrer une nouvelle instance si la tâche tourne déjà Ciblage par groupe de sécurité Admins du domaine

Ça n’a jamais fonctionné proprement. La tâche apparaissait systématiquement refusée dans le rapport gpresult détaillé, malgré un ciblage a priori correct (groupe et SID valides, compte bien membre d’Admins du domaine). Le déplacement de l’élément de préférence en Configuration ordinateur la faisait “descendre” — mais au prix de casser la logique même du montage (le contexte %LogonUser% n’est pas fiable en traitement côté ordinateur, où aucun utilisateur n’est nécessairement connecté au moment du traitement).

En essayant le traitement en boucle de rappel (loopback, mode Fusion) pour faire appliquer les paramètres utilisateur d’une GPO liée à l’OU du poste indépendamment de l’OU réelle du compte admin, puis en basculant sur un script d’ouverture de session classique en remplacement de la tâche planifiée GPP, j’ai fini par provoquer l’incident le plus concret du chantier : un script d’ouverture de session synchrone par défaut, pointant par erreur directement sur le watcher lui-même (qui ne se termine jamais, par conception) plutôt que sur un lanceur détaché, a bloqué l’ouverture de session d’un poste sur un écran noir fixe — Windows attendant indéfiniment la fin d’un script qui ne finit jamais.

La leçon a été suffisamment nette pour changer d’approche plutôt que de continuer à empiler des correctifs. Sur un périmètre limité à quelques comptes admin — pas les ~27 machines du parc — le rapport coût/bénéfice d’une GPO (filtrage par groupe, loopback, timing d’application, risque de blocage de session) était disproportionné. La solution retenue : une tâche planifiée locale, créée une fois par machine et par compte admin avec une simple commande PowerShell (Register-ScheduledTask), sans passer par une GPO. Moins élégant sur le papier qu’un déploiement centralisé, mais fiable, rapide à diagnostiquer, et sans risque de bloquer une session à l’échelle du domaine.

Test et validation

Une fois le watcher opérationnel (peu importe le mécanisme de démarrage), la chaîne complète a été validée de bout en bout : un .rdp créé sur le Bureau est détecté, signé via l’endpoint JEA, réécrit avec sa signature. Côté GPO, l’empreinte SHA1 du certificat de signature a été déclarée comme éditeur de confiance (Spécifier les empreintes SHA1 des certificats représentant les éditeurs de fichiers .rdp approuvés), d’abord testée en local via le registre (HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services, valeur CertificateThumbprints) avant tout déploiement GPO réel.

Résultat : le bandeau passe de rouge “Serveur de publication inconnu” à jaune, éditeur reconnu et nommé.

Éditeur reconnu — bandeau jaune après signature et déclaration de l’empreinte comme approuvée

La limite assumée

Un point m’a fait tourner en rond plus longtemps que nécessaire : le bandeau jaune “Vérifiez l’éditeur de cette connexion” réapparaît à chaque connexion, même pour un éditeur déclaré de confiance. J’ai testé plusieurs pistes avant de trouver la bonne explication : remise à zéro des propriétés de redirection (carte à puce, presse-papiers) sans effet, hypothèse que mstsc réécrivait ou effaçait la signature à la fermeture — infirmée par comparaison octet à octet du fichier avant/après —, ajout du certificat au magasin Éditeurs de confiance — sans effet non plus.

La bonne réponse, trouvée dans l’annonce officielle de Microsoft relayée par la presse spécialisée : ce bandeau réapparaît à chaque connexion par conception, y compris pour un éditeur de confiance déclaré. C’est un durcissement anti-hameçonnage délibéré, pas un défaut de configuration. Seul un tout autre message, pédagogique et affiché une seule fois par compte (“qu’est-ce qu’un fichier RDP”), est réellement mémorisé — jamais le bandeau de vérification d’éditeur lui-même.

L’objectif réel du chantier — éliminer le bandeau rouge “éditeur inconnu”, qui pousse au clic réflexe et casse la redirection carte à puce — est bien atteint. Le bandeau jaune qui persiste est le maximum atteignable dans ce nouveau modèle de sécurité, à documenter tel quel plutôt qu’à chercher à le faire disparaître.

Ce que je retiens

  • Un type de template hérité silencieusement (Utilisateur vs Ordinateur) à la duplication peut provoquer une erreur d’inscription totalement silencieuse — sans aucune trace côté journaux, ce qui la rend particulièrement coûteuse à diagnostiquer.
  • Centraliser une clé de signature plutôt que la disperser sur chaque poste vaut le coût d’un aller-retour réseau supplémentaire — surtout avec JEA, qui permet d’exposer une action précise sans ouvrir un accès administratif complet.
  • Un script d’ouverture de session qui ne se termine jamais ne doit jamais être appelé directement en mode synchrone — toujours passer par un lanceur détaché qui rend la main immédiatement.
  • Face à un mécanisme GPO qui résiste (ILT refusé, loopback capricieux, script bloquant), il vaut parfois mieux reconnaître qu’une solution plus simple et plus locale suffit largement au périmètre réel, plutôt que de s’obstiner sur l’architecture centralisée par principe.
  • Un comportement de sécurité qui semble être un bug après plusieurs tests qui l’infirment est parfois un choix de conception assumé par l’éditeur — vérifier la documentation officielle avant de continuer à chercher un contournement qui n’existe pas.

Sources et références