Votre script PowerShell vers Microsoft Graph échoue sur un serveur DMZ avec « The underlying connection was closed » ? Ce guide explique la cause (TLS), le correctif immédiat et la manière de pérenniser la configuration et d’auditer l’environnement.
Contexte et symptômes
Scénario typique : un script PowerShell envoie des e‑mails via l’API Microsoft Graph/Office 365. Il fonctionne sur un poste local et un premier serveur, mais échoue sur un serveur en DMZ.
Network connection test to Microsoft Graph API endpoint succeeded.
Error: The underlying connection was closed: An unexpected error occurred on a send.
Un journal pare‑feu remonte parfois tcp-rst-from-client
. Les équipes réseau n’observent aucun blocage évident (le port 443 est ouvert, la résolution DNS est correcte) et l’URL de Microsoft Graph répond ailleurs.
- Facteur aggravant : VM ancienne (Windows Server 2012 R2/2016) durcie, proxy sortant, inspection TLS, GPO de sécurité, .NET obsolète.
- Appels concernés :
Invoke-RestMethod
,Invoke-WebRequest
, modules maison utilisantSystem.Net.Http
ouHttpWebRequest
.
Cause racine (la plus fréquente)
Depuis octobre 2020, Microsoft Graph n’accepte plus TLS 1.0/1.1. Si la pile .NET de la machine source n’offre pas TLS 1.2 (ou supérieur) lors de la négociation, le serveur distant refuse la session pendant le “ClientHello” ou au tout début de l’échange, d’où la fermeture brutale et le message générique « underlying connection was closed ». Le tcp-rst-from-client
que vous voyez côté pare‑feu traduit ce rejet rapide par l’hôte externe ou un intermédiaire qui coupe la session.
En pratique, les causes se combinent fréquemment :
- .NET Framework < 4.6 ou strong crypto désactivé ➜ la valeur par défaut reste TLS 1.0.
- OS où TLS 1.2 n’est pas activé au niveau SChannel.
- Proxy ou boîtier d’inspection TLS incapable de relayer TLS 1.2 de bout en bout.
- Suites de chiffrement modernes filtrées par une GPO.
Correctif express (validé)
Avant tout Invoke-RestMethod
ou Invoke-WebRequest
, forcez explicitement la négociation vers TLS 1.2 (avec, si vous tenez à la rétro‑compatibilité interne, TLS 1.1). Placez cette ligne tout en haut de votre script :
[Net.ServicePointManager]::SecurityProtocol =
[Net.SecurityProtocolType]::Tls -bor
[Net.SecurityProtocolType]::Tls11 -bor
[Net.SecurityProtocolType]::Tls12
Recommandation : pour Microsoft Graph, privilégiez la version minimale suivante si vous contrôlez l’ensemble des cibles :
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Une fois cette directive ajoutée, l’appel Graph aboutit généralement immédiatement sur la machine problématique, ce qui confirme l’absence de TLS 1.2 en cause.
Exemple minimal d’appel Graph robuste
# 1) Forcer TLS 1.2 (Windows PowerShell 5.1 / .NET Framework)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# 2) Récupérer un jeton (ex. client credentials)
$tenantId = ''
$appId = ''
$appSecret = ''
$scope = '[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)'
$body = @{
client_id = $appId
client_secret = $appSecret
scope = $scope
grant_type = 'client_credentials'
}
$token = (Invoke-RestMethod -Method Post ` -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"`
-Body $body -ContentType 'application/x-www-form-urlencoded').access_token
# 3) Appeler Microsoft Graph (ex. envoi d’e-mail)
$headers = @{ Authorization = "Bearer $token" }
$payload = @{
message = @{
subject = "Test DMZ TLS12 OK"
body = @{ contentType = "Text"; content = "Hello from DMZ over TLS1.2" }
toRecipients = @(@{ emailAddress = @{ address = "[destinataire@exemple.com](mailto:destinataire@exemple.com)" } })
}
saveToSentItems = $true
}
Invoke-RestMethod -Method Post -Headers $headers ` -Uri "https://graph.microsoft.com/v1.0/users/<expediteur@domaine>/sendMail"`
-Body ($payload | ConvertTo-Json -Depth 10) -ContentType 'application/json'
Note PowerShell 7+ : PowerShell 7 utilise .NET (Core) moderne et négocie TLS 1.2 par défaut via
HttpClient
. Sur ces versions, la ligneServicePointManager
n’a pas toujours d’effet (pipeline différent), mais n’est pas nuisible. Si l’erreur persiste en 7+, visez plutôt la configuration OS/Proxy décrite ci‑dessous.
Procédure de diagnostic pas à pas
1) Vérifier versions OS / PowerShell / .NET
$PSVersionTable
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full').Release
Get-ComputerInfo | Select-Object WindowsProductName, OsVersion, OsBuildNumber
Le Release Key .NET indique la version exacte du Framework :
Release | .NET Framework | Comportement TLS par défaut |
---|---|---|
379893 | 4.5.2 | Souvent TLS 1.0 si non forcé |
393295/393297 | 4.6 | Peut suivre l’OS, mais pas strong crypto par défaut |
394802/394806 | 4.6.2 | Meilleur support TLS 1.2, configuration requise |
460798/460805 | 4.7 | Utilise davantage les paramètres OS |
461808/461814 | 4.7.2 | Fortement recommandé |
528040/528372+ | 4.8 | Recommandé : suit l’OS et gère bien TLS 1.2 |
2) Confirmer la négociation TLS côté machine
Testez rapidement la poignée de main TLS vers les hôtes critiques :
Test-NetConnection graph.microsoft.com -Port 443
Test-NetConnection login.microsoftonline.com -Port 443
Ces commandes valident la connectivité TCP, pas la version TLS. Pour contrôler la version, utilisez une petite fonction .NET qui force le protocole lors de la poignée de main :
function Test-TlsHandshake {
param(
[Parameter(Mandatory)]
[string]$Host,
[int]$Port = 443,
[System.Security.Authentication.SslProtocols]$Protocol = [System.Security.Authentication.SslProtocols]::Tls12
)
$client = New-Object System.Net.Sockets.TcpClient
$client.Connect($Host, $Port)
$stream = New-Object System.Net.Security.SslStream($client.GetStream(), $false, ({$true}))
try {
$stream.AuthenticateAsClient($Host, $null, $Protocol, $false)
Write-Host "Handshake OK via $Protocol sur $Host:$Port"
} catch {
Write-Warning "Handshake ECHEC via $Protocol sur $Host:$Port : $($_.Exception.Message)"
} finally {
$stream.Dispose()
$client.Close()
}
}
# Exemples
Test-TlsHandshake -Host graph.microsoft.com -Protocol Tls12
Test-TlsHandshake -Host login.microsoftonline.com -Protocol Tls12
Si TLS 1.2 échoue ici, le problème est local (OS, .NET, GPO, proxy).
3) Inspecter proxy/chemin réseau
netsh winhttp show proxy
$env:HTTPS_PROXY
$env:NO_PROXY
- Inspection TLS : si un proxy déchiffre le trafic sortant, il doit parler TLS 1.2 aux deux extrémités. Demandez un bypass explicite pour
graph.microsoft.com
etlogin.microsoftonline.com
si l’équipement ne gère pas correctement les suites modernes. - Proxy système vs script :
Invoke-RestMethod
suit WinHTTP par défaut en Windows PowerShell. Vous pouvez forcer-Proxy
ou-NoProxy
selon les besoins.
4) Examiner les journaux SChannel
Sur la machine source, consultez le journal SChannel (Journal Windows > Système) autour de l’heure de l’échec. Les événements de type 36874/36888 indiquent des alertes de poignée de main, ainsi que la suite de chiffrement et la version TLS négociées/refusées.
Rendre la correction pérenne
Forcer TLS 1.2 dans le script règle le symptôme. Pour éviter de dépendre d’un hack applicatif, normalisez l’OS et .NET.
Activer TLS 1.2 dans SChannel (niveau OS)
Activez explicitement les clés de registre (Client/Server) puis redémarrez :
# Client
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" -Force | Out-Null
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" `
-Name "Enabled" -Value 1 -PropertyType "DWord" -Force | Out-Null
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" `
-Name "DisabledByDefault" -Value 0 -PropertyType "DWord" -Force | Out-Null
# Server (utile si la machine héberge aussi des services)
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" -Force | Out-Null
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" ` -Name "Enabled" -Value 1 -PropertyType "DWord" -Force | Out-Null
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server"`
-Name "DisabledByDefault" -Value 0 -PropertyType "DWord" -Force | Out-Null
Durcir .NET Framework pour préférer TLS modernes
Sur Windows PowerShell (basé .NET Framework), ajoutez :
# 64 bits
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" `
-Name "SchUseStrongCrypto" -Value 1 -PropertyType "DWord" -Force | Out-Null
# 32 bits (si des processus x86 existent)
New-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319" `
-Name "SchUseStrongCrypto" -Value 1 -PropertyType "DWord" -Force | Out-Null
Avec .NET 4.7+, la clé SystemDefaultTlsVersions
peut être utilisée pour suivre l’OS :
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" `
-Name "SystemDefaultTlsVersions" -Value 1 -PropertyType "DWord" -Force | Out-Null
New-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319" `
-Name "SystemDefaultTlsVersions" -Value 1 -PropertyType "DWord" -Force | Out-Null
GPO et suites de chiffrement
- Vérifiez qu’aucune GPO ne désactive TLS 1.2 ou ne force des suites RSA static obsolètes.
- Sur Windows Server 2016+,
Get-TlsCipherSuite
liste les suites autorisées. Préférez ECDHE + AES‑GCM.
Tableau récapitulatif
Symptôme | Cause probable | Vérification | Remède |
---|---|---|---|
« The underlying connection was closed » | Négociation TLS < 1.2 | Test-TlsHandshake -Protocol Tls12 échoue | Forcer TLS 1.2 dans le script et activer TLS 1.2 côté OS |
tcp-rst-from-client sur le pare‑feu | Serveur Graph/proxy ferme après un ClientHello incompatible | Trames TLS ou journaux SChannel | Activer TLS 1.2 / corriger proxy |
Fonctionne en local, échoue en DMZ | DMZ sans TLS 1.2, GPO restrictive, inspection TLS | Comparaison des Release .NET / clés TLS | Normaliser .NET, GPO et exceptions proxy |
Erreur « Could not create SSL/TLS secure channel » | Problème de versions TLS ou de certificats | Validation de chaîne de certificats | Corriger magasin de certificats / protocole |
Bons réflexes de développement
- Toujours fixer TLS en tête de script si vous ciblez Windows PowerShell 5.1 :
# Tout en haut du script
if ($PSVersionTable.PSEdition -eq 'Desktop') {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
- Utiliser
HttpClient
moderne (PowerShell 7) quand c’est possible : meilleures perfs, TLS à jour, HTTP/2. - Éviter les globales surprises : si un module tiers modifie
SecurityProtocol
, restaurez‑le après l’appel. - Journaliser la version TLS réellement négociée pour accélérer les diagnostics ultérieurs.
Cas particuliers et pièges fréquents
- Certificat intercepté : un proxy d’inspection présente un certificat intermédiaire interne. Si la machine ne fait pas confiance à l’AC interne, vous verrez une erreur de validation de certificat, distincte de l’erreur TLS version. Importez la chaîne complète dans “Ordinateur local > Autorités de certification racines de confiance”.
- Vieilles bibliothèques .NET : certaines librairies embarquent leur propre pile HTTP/SSL. Mettez‑les à jour.
- Politique FIPS : elle ne bloque pas TLS 1.2 en soi, mais peut influencer les suites autorisées. Vérifiez les GPO.
- Équilibrage/Offload SSL : si un proxy termine TLS et ré‑établit une session sortante, il doit lui‑même parler TLS 1.2 vers Internet.
Vérifications rapides à script unique
# 1) OS / .NET
"$([Environment]::OSVersion.VersionString) | .NET Release: " + `
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full').Release
# 2) TLS 1.2 OS activé ? (reg)
Get-Item "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" -ErrorAction SilentlyContinue |
Get-ItemProperty | Format-List *
# 3) Handshake TLS12 vers Graph
Test-TlsHandshake -Host graph.microsoft.com -Protocol Tls12
# 4) Appel Graph trivial (HEAD) - utile pour valider rapidement le pipeline réseau
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Method Head -Uri "[https://graph.microsoft.com/v1.0](https://graph.microsoft.com/v1.0)" -UseBasicParsing | Out-Null
"Appel Graph HEAD OK en TLS1.2"
} catch { "Echec appel Graph: $($_.Exception.Message)" }
Mise à jour conseillée
- .NET Framework 4.8 sur les serveurs Windows PowerShell 5.1.
- PowerShell 7.x pour les nouveaux développements (lorsque compatible avec vos modules).
- Durcissement OS via TLS 1.2 activé par défaut et suites modernes ECDHE + AES‑GCM.
FAQ
Q : Pourquoi cela marche sur un serveur et pas sur un autre ?
R : Les piles TLS/NET diffèrent (version .NET, clés de registre, GPO, proxy). Le serveur “OK” expose TLS 1.2 par défaut, l’autre retombe sur TLS 1.0.
Q : Le message « underlying connection was closed » ne parle pas de TLS, comment l’être sûr ?
R : C’est un message générique. Confirmez via Test-TlsHandshake
, journaux SChannel et en forçant TLS 1.2. Si l’erreur disparaît, la cause était la négociation TLS.
Q : Ajouter TLS 1.1 dans la liste est‑il utile ?
R : Microsoft Graph n’accepte plus 1.1, mais l’indiquer ne nuit pas si 1.2 est proposé en premier. Pour éviter tout doute, contentez‑vous de Tls12
.
Q : Et TLS 1.3 ?
R : TLS 1.3 est supporté selon l’OS et la pile .NET moderne. Windows PowerShell 5.1 (.NET Framework) ne permet pas de le forcer via ServicePointManager
. Visez TLS 1.2 a minima.
Bonnes pratiques d’exploitation
- Documenter la baseline TLS (versions/suites) par zone (LAN/DMZ) et la valider à chaque changement réseau.
- Ajouter un test de santé Graph (HEAD/GET simple) dans vos sondes de supervision.
- Automatiser l’application des clés de registre TLS via GPO/Desired State Configuration.
- Journaliser les échecs d’auth Graph et l’horodatage pour corrélation avec les logs réseau.
Résumé opérationnel
- Ajouter la ligne TLS dans le script PowerShell (
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
). - Tester : si l’appel aboutit, la cause était la négociation TLS.
- Pérenniser : activer TLS 1.2 côté OS, mettre à jour .NET/PowerShell, ajuster les GPO.
- Contrôler les composants réseau (proxy, inspection TLS) pour éviter de futurs refus.
Annexe : mémo des clés et commandes utiles
Aspect | Commande / Clé | But |
---|---|---|
Version .NET | (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full').Release | Identifier le Framework installé |
TLS 1.2 (OS) | HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client|Server | Activer la prise en charge TLS 1.2 |
.NET Strong Crypto | HKLM\SOFTWARE\Microsoft\.NETFramework\v4.0.30319\SchUseStrongCrypto | Forcer les algorithmes modernes |
Suites TLS | Get-TlsCipherSuite | Inventorier les suites autorisées |
Proxy WinHTTP | netsh winhttp show proxy | Valider la configuration proxy système |
Test handshake | Test-TlsHandshake -Host graph.microsoft.com -Protocol Tls12 | Vérifier la négociation TLS 1.2 |
Avec ces actions, vous éliminez l’essentiel des erreurs « The underlying connection was closed: An unexpected error occurred on a send » lors des appels PowerShell vers Microsoft Graph depuis des environnements DMZ.