Un’app .NET che invia email via Microsoft Identity Client e Exchange Online funziona da console ma fallisce quando eseguita da Utilità di pianificazione, lasciando nel registro l’evento SChannel ID 36882. In questa guida trovi cause probabili, diagnosi e correzioni affidabili per eliminare l’errore di handshake TLS.
Scenario e sintomi
Un eseguibile .NET (framework o .NET modern) invia posta tramite Exchange Online autenticandosi con MSAL e un certificato client. Avviato a mano dall’utente interattivo, tutto ok; quando lo stesso binario è lanciato da Utilità di pianificazione con lo stesso account o un account di servizio, l’invio fallisce. Nel registro eventi compaiono voci SChannel con Event ID 36882 (fallimento del TLS handshake) e, talvolta, ulteriori eventi come 36888 con codici di alert (per esempio handshake_failure). L’app può anche sollevare eccezioni generiche di rete, time‑out o “The request was aborted: Could not create SSL/TLS secure channel”.
Cause tecniche più probabili
Contesto | Motivo ricorrente |
---|---|
Store certificati | Il processo pianificato gira in un contesto di servizio (ad esempio NT AUTHORITY\SYSTEM o un service account) che non ha accesso al Personal Store dell’utente interattivo. Manca quindi il certificato client usato da MSAL/SMTP o non è accessibile con la relativa chiave privata. |
Privilegi | Il task non è eseguito con token sufficienti ad aprire la chiave privata del certificato o a iniziare la negoziazione TLS. L’opzione predefinita “Esegui solo se l’utente è connesso” può produrre token con capacità ridotte. |
Impostazioni TLS | Il computer o il processo usano protocolli/cifrari obsoleti. Se TLS 1.2 o cifrari con PFS non sono attivi, SChannel rifiuta la negoziazione e registra 36882. |
Proxy e rete | Un task non interattivo usa spesso le impostazioni WinHTTP e non il proxy dell’utente (WinINET). Senza proxy corretto il TLS handshake verso login/Graph/Exchange fallisce. |
Permessi sulla chiave | Il certificato è nel Computer Store ma l’account del task non ha permessi di lettura sulla private key associata. |
Profilo utente | Il logon di tipo batch può non caricare un profilo completo. Componenti che usano DPAPI o archivi utente non trovano i materiali crittografici. |
Orario di sistema | Uno skew NTP marcato può rompere TLS o OAUTH (token non validi), producendo errori simili durante il handshake. |
Diagnosi rapida e verifiche di base
- Confronta il contesto: esegui lo stesso binario da console e da task. Nel codice aggiungi una stampa di
Environment.UserName
eWindowsIdentity.GetCurrent().Name
. - Controlla il proxy: in una console elevata esegui
netsh winhttp show proxy
. Se vuoto ma l’utente usa un proxy, importa o configura quello di sistema. - Ispeziona SChannel: in Event Viewer → Windows Logs → System → SChannel filtra per ID 36882/36888 e annota ora e codice alert.
- Verifica il certificato: sotto Certificati (computer) → Personale conferma presenza del certificato con chiave privata.
- Test TLS: da PowerShell, verifica con
Test-NetConnection login.microsoftonline.com -Port 443
eTest-NetConnection outlook.office365.com -Port 443
.
Soluzioni operative
Configurazione corretta del task
- Apri Utilità di pianificazione, proprietà del task.
- Nella scheda Generale seleziona Esegui con i privilegi più elevati.
- Seleziona Esegui indipendentemente dalla sessione utente (Run whether user is logged on or not) per ottenere un token completo.
- Imposta Configura per la tua versione di Windows Server/Windows.
- Compila il campo Avvia in (cartella di lavoro) con la directory dell’eseguibile per evitare problemi di path relativi.
- Nella scheda Condizioni disattiva “avvia solo se su alimentazione di rete” o condizioni non pertinenti.
- Nella scheda Impostazioni abilita “Consenti esecuzioni forzate” e “Riavvia in caso di errore” con un numero di tentativi ragionevole.
Gestione del certificato e dello store
Per evitare differenze tra contesto interattivo e servizio, mantieni il certificato nel Computer Store (LocalMachine\My
) e non nello store utente.
Procedura da MMC
- Apri mmc.exe → File → Aggiungi/Rimuovi snap-in → Certificati → Account computer.
- Importa il certificato in Personale. Assicurati che compaia la dicitura Hai la chiave privata che corrisponde a questo certificato.
- Click destro sul certificato → Tutte le attività → Gestisci chiavi private e assegna all’account del task il permesso di Lettura sulla chiave.
Procedura con PowerShell
# Esporta da CurrentUser\My e importa nel LocalMachine\My
$thumb = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # thumbprint
$pwd = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
Export-PfxCertificate ` -Cert "Cert:\CurrentUser\My\$thumb"`
-FilePath "C:\temp\client.pfx" `
-Password $pwd
Import-PfxCertificate ` -FilePath "C:\temp\client.pfx"`
-CertStoreLocation "Cert:\LocalMachine\My" `
-Password $pwd
Verifica presenza chiave privata
Get-ChildItem Cert:\LocalMachine\My$thumb |
Select-Object Subject, Thumbprint, HasPrivateKey
Nota: se lo spostamento non è possibile, duplica il certificato (con chiave privata) nel Computer Store. Evita di lasciare PFX su disco; elimina il file dopo l’import e usa password robuste.
Riparazione associazione chiave
Se il certificato appare senza chiave privata (freccia gialla), ripara l’associazione:
certutil -repairstore my <THUMBPRINT>
Proxy e connettività nei processi non interattivi
I processi avviati da Utilità di pianificazione usano spesso le impostazioni WinHTTP. Se a livello utente è configurato un proxy ma WinHTTP è “Direct”, il TLS handshake verso i servizi Microsoft fallirà.
:: Visualizza proxy WinHTTP
netsh winhttp show proxy
:: Importa le impostazioni del proxy dell’utente corrente
netsh winhttp import proxy source=ie
:: Oppure imposta esplicitamente
netsh winhttp set proxy proxy-server="http=myproxy:8080;https=myproxy:8080" bypass-list="*.contoso.local"
Se esiste un proxy TLS-intercept, assicurati che login.microsoftonline.com, graph.microsoft.com ed endpoint Exchange Online siano esclusi dall’ispezione. L’intercettazione può rompere il TLS handshake e generare 36882.
Impostazioni TLS moderne su Windows
Assicurati che TLS 1.2 (o superiore) sia abilitato su client e che i cipher suite ECDHE siano disponibili.
Abilitazione via registro
[HKEYLOCALMACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client]
"Enabled"=dword:00000001
"DisabledByDefault"=dword:00000000
[HKEYLOCALMACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server]
"Enabled"=dword:00000001
"DisabledByDefault"=dword:00000000
Uso dei cipher suite corretti
Exchange Online richiede cifrari moderni con Perfect Forward Secrecy (ECDHE). Evita scenari in cui siano rimasti solo “TLSRSA*”. Se stai forzando un ordine di cipher via criteri, verifica che includa TLSECDHERSAWITHAES128GCM_SHA256
(o equivalenti) e che non sia presente un filtro restrittivo lato endpoint di rete.
Impostazioni .NET per TLS e sicurezza
Per .NET Framework (app a 32/64 bit), abilita l’uso dei protocolli di sistema e dei cifrari robusti:
[HKEYLOCALMACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001
"SystemDefaultTlsVersions"=dword:00000001
[HKEYLOCALMACHINE\SOFTWARE\WOW6432Node\Microsoft.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001
"SystemDefaultTlsVersions"=dword:00000001
In alternativa, solo per test, puoi forzare TLS 1.2 all’avvio del processo:
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
Su .NET 6+ l’handler HTTP usa per default i protocolli abilitati a livello OS; la priorità è quindi configurare correttamente SChannel.
Attivazione del logging diagnostico
- SChannel: imposta
EventLogging=1
inHKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
, quindi riavvia il servizio o il sistema per aumentare la verbosità. - MSAL: in fase di test abilita PII con
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
e un callback di logging. Disattiva in produzione. - Task Scheduler: abilita il log Microsoft-Windows-TaskScheduler/Operational per vedere il contesto del logon.
Trigger alternativi per isolamento del problema
Se il fallimento avviene solo con trigger basati sul tempo, prova un trigger su evento (per esempio su un ID di un log precedente) per escludere dipendenze da stato di logon o di blocco schermo. Il cambio di trigger aiuta a distinguere problemi di token/privilegi da quelli di rete.
Guida passo‑passo consolidata
- Imposta il task con privilegi elevati e “Esegui indipendentemente dalla sessione utente”. Specifica l’account definitivo (service account se possibile).
- Sposta o duplica il certificato nel
LocalMachine\Personal
. Verifica con:certutil -store my
Controlla che sia presente “Key Prov Info” o “Private key is present”. - Concedi permessi sulla chiave privata all’account del task tramite MMC → Manage Private Keys.
- Allinea proxy WinHTTP a quello utente o configura l’eccezione per gli endpoint Microsoft.
- Abilita e verifica TLS 1.2 e la presenza di cipher ECDHE nelle impostazioni SChannel.
- Conferma .NET strong crypto e, se legacy, forza TLS 1.2 nel codice per i test.
- Verifica l’orologio e la sincronizzazione NTP (
w32tm /query /status
). - Riesegui e raccogli log da SChannel, Task Scheduler e applicazione.
Esempio di codice MSAL con certificato nel Computer Store
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Logging;
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
#if NETFRAMEWORK
// Per .NET Framework
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
#endif
IdentityModelEventSource.ShowPII = true; // Solo per test
```
var tenantId = "<TENANT_GUID_O_NOME>";
var clientId = "<APP_ID_CLIENT>";
var thumb = "<THUMBPRINT_CERT>";
X509Certificate2 cert = null;
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
cert = store.Certificates
.Find(X509FindType.FindByThumbprint, thumb, false)
.OfType<X509Certificate2>()
.FirstOrDefault(c => c.HasPrivateKey);
}
if (cert == null) throw new Exception("Certificato non trovato o senza chiave privata.");
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithCertificate(cert)
.Build();
string[] scopes = new[] { "https://outlook.office365.com/.default" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
Console.WriteLine($"Token acquisito, scadenza: {result.ExpiresOn}");
}
```
}
Verifica dell’ambiente con PowerShell
# SChannel: ultimi eventi 36882/36888
$filter = @{
LogName='System'
ProviderName='Schannel'
Id=36882,36888
StartTime=(Get-Date).AddDays(-1)
}
Get-WinEvent -FilterHashtable $filter |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-List
Proxy WinHTTP
netsh winhttp show proxy
TLS 1.2 su .NET Framework (registro)
reg query "HKLM\SOFTWARE\Microsoft.NETFramework\v4.0.30319" /v SchUseStrongCrypto
reg query "HKLM\SOFTWARE\Microsoft.NETFramework\v4.0.30319" /v SystemDefaultTlsVersions
Verifica porte e reachability
Test-NetConnection login.microsoftonline.com -Port 443
Test-NetConnection outlook.office365.com -Port 443
Approfondimento su differenze di contesto
Il cuore del problema è la disomogeneità del contesto tra esecuzione interattiva e servizio:
- Store dei certificati: CurrentUser\My è legato al profilo utente interattivo; un service account o
SYSTEM
non vede quel certificato. Spostarlo o duplicarlo in LocalMachine\My elimina la dipendenza dal profilo. - DPAPI: le chiavi utente sono protette dalle credenziali del profilo. Un logon batch potrebbe non avere accesso al master key.
- Proxy: WinINET (utente) vs WinHTTP (sistema). Un proxy assente o differente produce time‑out e failure del TLS handshake prima ancora che MSAL possa ottenere un token.
- Permessi sulla private key: anche se il certificato è nel Computer Store, l’ACL della chiave in
%ProgramData%\Microsoft\Crypto\Keys
deve includere l’account del task.
Check di sicurezza e buone pratiche
- Minimizza i privilegi: usa account di servizio dedicati e limita la lettura della chiave privata al solo account necessario.
- Proteggi i PFX: se devi esportare, usa password robuste e cancella il file al termine (
Remove-Item
sicuro). - Niente hard‑coding di segreti: non inserire thumbprint/tenant in chiaro se non strettamente necessario; valuta variabili d’ambiente o configurazione cifrata.
- Monitora SChannel: lascia
EventLogging
a 1 solo durante la diagnosi per evitare rumore nei log.
Domande frequenti
Perché manualmente funziona ma come task no?
Perché cambiano store dei certificati, token e proxy. Il processo ha ambiente, profilo, ACL e impostazioni TLS diverse. Il caso più comune è la mancanza della chiave privata nel contesto del servizio.
Posso risolvere solo forzando ServicePointManager.SecurityProtocol
?
È un palliativo per app .NET Framework legacy. La correzione strutturale è abilitare TLS 1.2 a livello OS e allineare proxy/ACL certificato.
Quali cipher suite devo abilitare?
Mantieni una lista moderna con ECDHE e AES‑GCM. Evita di lasciare attivi solo i vecchi TLSRSA*
. In presenza di un ordine forzato, inserisci in testa suite ECDHE con AES‑GCM a 128 o 256.
Serve il certificato anche per SMTP?
Il certificato è usato da MSAL per l’autenticazione applicativa e il rilascio del token OAuth2. Lo scambio SMTP/Graph usa poi quel token. Se il certificato non è accessibile nel contesto del task, l’intero flusso fallisce prima del TLS handshake “applicativo”.
Lista di controllo pronta all’uso
- Task con Run whether user is logged on or not e Run with highest privileges.
- Certificato duplicato in
LocalMachine\My
con chiave privata. - Permesso di lettura sulla private key per l’account del task.
- TLS 1.2 abilitato su SChannel e .NET strong crypto attivo.
- Proxy WinHTTP allineato o esclusioni corrette.
- Orologio sincronizzato e CRL/OCSP raggiungibili.
- Logging SChannel e MSAL abilitato per verifica.
Esempio di script per schedulare correttamente
$action = New-ScheduledTaskAction -Execute "C:\Apps\Mailer\Mailer.exe" -WorkingDirectory "C:\Apps\Mailer"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5)
$principal = New-ScheduledTaskPrincipal -UserId "DOMAIN\svc-mailer" -LogonType Password -RunLevel Highest
Register-ScheduledTask -TaskName "MailerTask" -Action $action -Trigger $trigger -Principal $principal -Description "Invio email via Exchange Online con MSAL"
Riepilogo operativo
L’evento SChannel 36882 in uno Scheduled Task è quasi sempre dovuto a differenze di contesto tra esecuzione interattiva e servizio. Sposta o duplica il certificato nel Computer Store, esegui il task con privilegi elevati, abilita TLS 1.2 (o superiore) e allinea il proxy WinHTTP. Queste azioni risolvono la maggior parte dei casi in produzione.
Appendice di comandi utili
:: Verifica certificati nel Computer Store
certutil -store my
:: Verifica orologio
w32tm /query /status
:: Forza aggiornamento criteri
gpupdate /force
:: Avvio manuale del task per test
schtasks /run /tn "MailerTask"
Note finali
- Se il problema scompare quando esegui l’app in sessione interattiva ma riappare con il trigger temporizzato, focalizzati su proxy WinHTTP, permessi della chiave privata e impostazioni TLS a livello di sistema.
- Se vedi eventi 36870 o 36869, il problema è quasi certamente la chiave privata del certificato (mancante o non accessibile).
- Nel dubbio, crea un task “di prova” che stampa identità, variabili d’ambiente e versione di .NET per confrontare i due contesti.
Soluzioni operative sintetizzate
- Configurare il task
– Spunta “Esegui con i privilegi più elevati”.
– Imposta “Esegui indipendentemente dalla sessione utente (Run whether user is logged on or not)”.
– Specifica preferibilmente un account di servizio che possiede il certificato in LocalMachine\Personal. - Spostare o duplicare il certificato
– Copia il certificato client e la relativa chiave privata in Computer Account → Personal.
– Verifica concertutil -store my
che lo store contenga la chiave (Key Prov Info). - Verificare i protocolli TLS
– Abilita TLS 1.2/1.3 via registro o criteri:HKLM\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server → Enabled=1
.
– Assicurati che non restino solo cipher “TLSRSA*”: Exchange Online richiede cifrari con PFS. - Controllare l’attivazione del trigger
– Prova un trigger su evento per escludere dipendenze da logon o schermo bloccato. - Logging diagnostico
– Abilita SChannel event logging:HKLM\...\SCHANNEL → EventLogging = 1
.
– Abilita MSAL logging conIdentityModelEventSource.ShowPII = true
in test per identificare la fase esatta del TLS handshake.
Con questi accorgimenti, la catena TLS → MSAL → Exchange Online resta coerente tra esecuzione interattiva e pianificata, eliminando gli errori di handshake SChannel ID 36882.