E_ACCESSDENIED avec Import‑Certificate vers TrustedPublisher (PowerShell Direct/Hyper‑V) : causes, correctifs et scripts

Vous importez un certificat dans TrustedPublisher via PowerShell Direct sur une VM Hyper‑V et obtenez UnauthorizedAccessException: 0x80070005 (E_ACCESSDENIED) ? Voici une analyse approfondie et des correctifs reproductibles, avec scripts prêts à l’emploi et bonnes pratiques d’automatisation.

Sommaire

Contexte et symptômes

Dans une VM Hyper‑V fraîchement installée, un script PowerShell exécuté au travers de PowerShell Direct (par exemple Invoke-Command -Session $session ou Enter-PSSession -VMName) tente d’exécuter :

Import-Certificate -FilePath ".\adafruit_industries.cer" `
                   -CertStoreLocation "Cert:\LocalMachine\TrustedPublisher"

Bien que le script s’exécute sous le compte Administrateur local, la commande échoue avec :

UnauthorizedAccessException : 0x80070005 (E_ACCESSDENIED)

Observations usuelles :

  • Le même script fonctionne dans Root (Cert:\LocalMachine\Root).
  • Il fonctionne également dans TrustedPublisher si l’on ouvre d’abord une session graphique interactive dans la VM, puis on relance le script.

Pourquoi l’accès est-il refusé ? (analyse détaillée)

La combinaison « VM neuve + session distante via PowerShell Direct + import dans TrustedPublisher » révèle plusieurs conditions rarement documentées. Les causes sont cumulatives : corriger l’une ou l’autre peut suffire suivant l’environnement.

CauseExplication approfondieSymptômes/indices
Magasin TrustedPublisher non matérialiséLa ruche HKLM\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher et son sous‑dossier Certificates sont générés à la première utilisation. Sur des images « proprement » sysprepées, ils peuvent ne pas exister avant un premier accès interactif à la Console des certificats ou une première opération CAPI qui force la création. Lorsque le magasin n’existe pas, certains appels via .NET/PowerShell renvoient un E_ACCESSDENIED quelque peu trompeur.Get-ChildItem HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher renvoie « Not Found » ou montre une clé vide ; l’import via certutil fonctionne quand même.
Filtrage du jeton Administrateur (UAC)En session distante, même le compte Administrateur local peut recevoir un jeton filtré (token filtering) dépourvu de certains privilèges sensibles. L’import dans des magasins « protégés » (comme TrustedPublisher) peut exiger un jeton pleinement élevé.whoami /priv montre des privilèges désactivés ; whoami /groups inclut Local account and member of Administrators group.
Paramètre LocalAccountTokenFilterPolicyPar défaut (0), Windows applique des restrictions supplémentaires aux connexions distantes de comptes locaux, ce qui ampute le jeton de certains droits. Activer la valeur 1 restaure un comportement plus permissif.Après passage à 1 et redémarrage, l’import réussit sans autre changement.

Solutions express (choisir la plus adaptée)

Voici un résumé des options éprouvées. Elles ne sont pas exclusives : combinez‑les si nécessaire.

OptionCommande / mise en œuvreQuand l’utiliserAvantagesPrécautions
Pré‑créer le magasinNew-Item -Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher' -Force | Out-Null New-Item -Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher\Certificates' -Force | Out-Null Import-Certificate -FilePath '.\adafruit_industries.cer' ` -CertStoreLocation 'Cert:\LocalMachine\TrustedPublisher'Machines neuves, image de référence, exécution sans session graphique préalable.Idempotent, simple, aucun outil tiers.Exige tout de même un jeton suffisamment élevé.
Exécuter en LocalSystem (SYSTEM)Via tâche planifiée (intégré, pas d’outil tiers) : $action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Temp\InstallCert.ps1' $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest $task = New-ScheduledTask -Action $action -Principal $principal Register-ScheduledTask -TaskName 'InstallTrustedPublisherCert' -InputObject $task Start-ScheduledTask -TaskName 'InstallTrustedPublisherCert'Environnements verrouillés, pas d’administrateur interactif.SYSTEM n’est pas soumis à l’UAC ; fiabilité maximale.Orchestration un peu plus lourde (copier le script, créer/ouvrir la tâche).
Désactiver le filtrage UAC pour comptes locaux distantsreg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System` /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f shutdown /r /t 0Lab/DEV, pipelines d’intégration, hôtes isolés.Restaure un jeton complet pour l’Administrateur local à distance.Moins strict sur le plan sécurité. Éviter en production si possible.
Utiliser certutilcertutil -addstore -f "TrustedPublisher" ".\adafruit_industries.cer"Lorsque Import-Certificate échoue mais que CAPI bas niveau passe.Contourne certains échecs de l’enveloppe .NET/PowerShell.Vérifier le code retour : 0 indique la réussite.
Intégrer via GPOUtiliser une GPO « Trusted Publishers » ou « Certificate Services Client — Automatique » avec déploiement du certificat dans Ordinateur local.Parc d’ordinateurs, image maître, VDI.Traçabilité, conformité, pas d’élévation ad‑hoc.Requiert un domaine AD et une politique de déploiement.

Procédure de diagnostic pas‑à‑pas

  1. Confirmer l’existence du magasin Test-Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher' Test-Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher\Certificates' Si l’un des deux renvoie False, pré‑créez les clés puis réessayez l’import.
  2. Vérifier le jeton et les privilèges whoami /groups whoami /priv Si vous voyez un jeton restreint, essayez soit l’exécution en SYSTEM, soit l’option LocalAccountTokenFilterPolicy.
  3. Tenter un import bas niveau certutil -addstore -f "TrustedPublisher" ".\adafruit_industries.cer" Si certutil réussit mais PowerShell échoue, privilégiez certutil dans votre automation (ou investiguez les modules/.NET installés).
  4. Tracer avec CAPI2 Activez le journal Microsoft‑Windows‑CAPI2/Operational pour obtenir une chronologie détaillée des échecs d’accès au magasin.

Script idempotent « tout‑en‑un »

Le script ci‑dessous couvre la majorité des scénarios : création du magasin, import via PowerShell ou bascule automatique vers certutil, vérification de la présence du certificat, journalisation explicite. Vous pouvez l’exécuter en Administrateur (ou en SYSTEM via tâche planifiée).

param(
  [Parameter(Mandatory)]
  [ValidateScript({ Test-Path $_ })]
  [string]$FilePath,

[switch]$ForceCertutilFallback
)

function Get-CertThumbprint {
param([string]$Path)
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $Path))
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($bytes)
return $cert.Thumbprint.ToUpperInvariant()
}

function Ensure-TrustedPublisherStore {
$base = 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher'
if (-not (Test-Path $base)) { New-Item -Path $base -Force | Out-Null }
$certs = Join-Path $base 'Certificates'
if (-not (Test-Path $certs)) { New-Item -Path $certs -Force | Out-Null }
}

function Test-CertPresent {
param([string]$Thumbprint)
try {
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @('TrustedPublisher','LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$found = $store.Certificates | Where-Object { $_.Thumbprint -eq $Thumbprint }
$store.Close()
return [bool]$found
} catch {
return $false
}
}

function Add-Cert-TrustedPublisher {
param([string]$Path, [switch]$PreferCertutil)

Ensure-TrustedPublisherStore

if ($PreferCertutil) {
Write-Host 'Tentative via certutil...'
$p = Start-Process -FilePath 'certutil.exe' -ArgumentList @('-addstore','-f','TrustedPublisher',"`"$Path`"") -NoNewWindow -PassThru -Wait
return $p.ExitCode
}

try {
Import-Certificate -FilePath $Path -CertStoreLocation 'Cert:\LocalMachine\TrustedPublisher' -ErrorAction Stop | Out-Null
return 0
} catch {
Write-Warning "Import-Certificate a échoué : $($_.Exception.Message)"
Write-Host 'Bascule vers certutil...'
$p = Start-Process -FilePath 'certutil.exe' -ArgumentList @('-addstore','-f','TrustedPublisher',"`"$Path`"") -NoNewWindow -PassThru -Wait
return $p.ExitCode
}
}

# --- Main ---

$thumb = Get-CertThumbprint -Path $FilePath
if (Test-CertPresent -Thumbprint $thumb) {
Write-Host "Déjà présent dans TrustedPublisher : $thumb"
exit 0
}

$code = Add-Cert-TrustedPublisher -Path $FilePath -PreferCertutil:$ForceCertutilFallback
if ($code -ne 0) {
Write-Error "Échec de l'import (code retour $code). Consultez le journal CAPI2."
exit $code
}

if (Test-CertPresent -Thumbprint $thumb) {
Write-Host "Import réussi : $thumb"
exit 0
} else {
Write-Error "Import non vérifié malgré un code retour $code."
exit 1
}

Exécution en SYSTEM sans outil tiers (tâche planifiée)

Lorsque l’UAC ou le filtrage du jeton gêne, faites exécuter le script précédent par le compte LocalSystem via le Planificateur de tâches :

  1. Copiez le script dans C:\Temp\InstallCert.ps1 ainsi que le fichier .cer dans C:\Temp.
  2. Dans une session PowerShell (hôte ou VM), lancez :
$path = 'C:\Temp\adafruit_industries.cer'
$cmd  = "-NoProfile -ExecutionPolicy Bypass -File C:\Temp\InstallCert.ps1 -FilePath `"$path`""

$action    = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument $cmd
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest
$task      = New-ScheduledTask -Action $action -Principal $principal

Register-ScheduledTask -TaskName 'InstallTrustedPublisherCert' -InputObject $task -Force
Start-ScheduledTask -TaskName 'InstallTrustedPublisherCert'
Start-Sleep -Seconds 5
Get-ScheduledTask -TaskName 'InstallTrustedPublisherCert' | Get-ScheduledTaskInfo | Format-List *

Une fois terminé, supprimez la tâche et les fichiers temporaires pour ne pas polluer l’image.

Méthode GPO (déploiement à grande échelle)

Pour une image de référence ou un parc, évitez les scripts ad‑hoc et passez par la stratégie de groupe :

  • Créez un objet GPO ciblant le Contexte ordinateur.
  • Déployez le certificat dans Ordinateur > Paramètres Windows > Paramètres de sécurité > Stratégies de clés publiques > Éditeurs de confiance.
  • Vérifiez l’application via gpresult /h et certlm.msc.

Avantages : traçabilité, conformité, révocation centralisée, logs unifiés.

Comparatif Import-Certificate vs certutil

CritèreImport-Certificatecertutil
Niveau d’abstractionCmdlet .NET/PowerShell (facile à intégrer).Outil CAPI natif (bas niveau, très robuste).
DépendancesModules PowerShell & API .NET sous‑jacentes.Inclus avec Windows (pas de modules requis).
Résilience magasin « lazily created »Peut échouer si la clé registre n’existe pas.Tend à réussir, crée/force mieux le magasin.
Gestion des codes retourExceptions PowerShell (try/catch).Codes numériques (0 = succès), journal CAPI2.

Checklist de durcissement et de bonnes pratiques

  • Ne désactivez pas globalement l’UAC pour « faire marcher » un import ponctuel. Préférez l’exécution sous SYSTEM ou la GPO.
  • Conservez un dépôt versionné des fichiers .cer (Git, artefacts CI) et signez vos scripts d’installation.
  • Validez la chaîne : assurez‑vous que l’AC racine (ou intermédiaire) de l’éditeur est dans Root, sinon la signature de l’éditeur ne sera pas considérée comme approuvée même si le certificat est présent dans TrustedPublisher.
  • Journalisez systématiquement l’empreinte (thumbprint) importée et vérifiez‑la après import.
  • Nettoyez l’image : supprimez tâches, scripts et certificats temporaires une fois l’opération terminée.
  • Automatisez l’import avant la généralisation de l’image (audit mode / OOBE) pour éviter d’avoir à ouvrir une session graphique.

FAQ rapide

Pourquoi l’import réussit dans Root mais échoue dans TrustedPublisher ?
Les exigences d’accès aux magasins diffèrent. TrustedPublisher est rarement initialisé sur une image neuve et peut nécessiter un jeton plus « complet ». Root est quasi toujours présent.

Pourquoi ça marche dès que j’ouvre une session graphique dans la VM ?
La première session interactive provoque souvent la création paresseuse des clés du magasin. Ensuite, Import-Certificate fonctionne au prochain passage.

Dois‑je redémarrer après avoir défini LocalAccountTokenFilterPolicy=1 ?
Le changement est parfois pris en compte immédiatement, mais un redémarrage garantit l’application et simplifie le dépannage.

Est‑il risqué d’activer LocalAccountTokenFilterPolicy=1 ?
Oui, c’est moins strict pour les connexions distantes du compte local. Limitez‑le aux environnements isolés/lab ou utilisez‑le de façon transitoire durant l’automatisation.

Comment retirer un certificat « TrustedPublisher » en cas d’erreur ?
Utilisez l’empreinte pour le supprimer :

$thumb = 'VOTRE_EMPREINTE_SANS_ESPACES'
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @('TrustedPublisher','LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$cert = $store.Certificates | Where-Object { $_.Thumbprint -eq $thumb }
if ($cert) { $store.Remove($cert) }
$store.Close()

Exemple complet de pipeline (PowerShell Direct)

Le bloc suivant illustre une séquence robuste depuis l’hôte Hyper‑V : création du magasin, copie du certificat, import (avec repli), contrôle de présence, puis nettoyage. À adapter à votre orchestrateur.

# Sur l'hôte Hyper-V
$vmName   = 'MaVM'
$certPath = 'C:\Temp\adafruit_industries.cer'

# Ouvrir une session PowerShell Direct

$session = New-PSSession -VMName $vmName -Credential (Get-Credential)

# Copier le .cer dans la VM

Copy-Item -Path $certPath -Destination 'C:\Temp' -ToSession $session -Force

# Exécuter l'import robuste dans la VM

Invoke-Command -Session $session -ScriptBlock {
param($path)
New-Item -Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher' -Force | Out-Null
New-Item -Path 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\TrustedPublisher\Certificates' -Force | Out-Null

try {
Import-Certificate -FilePath $path -CertStoreLocation 'Cert:\LocalMachine\TrustedPublisher' -ErrorAction Stop | Out-Null
} catch {
Start-Process -FilePath 'certutil.exe' -ArgumentList @('-addstore','-f','TrustedPublisher',"`"$path`"") -NoNewWindow -Wait
}

# Vérification

$bytes = [System.IO.File]::ReadAllBytes($path)
$c = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($bytes)
$tp = $c.Thumbprint.ToUpperInvariant()

$store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @('TrustedPublisher','LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$ok = $store.Certificates | Where-Object { $_.Thumbprint -eq $tp }
$store.Close()

if ($ok) { "OK: $tp" } else { throw "Échec: $tp introuvable" }
} -ArgumentList 'C:\Temp\adafruit_industries.cer'

Erreurs connexes à connaître

  • 0x800B0109 (CERT_E_UNTRUSTEDROOT) : la chaîne de confiance n’est pas complète (racine manquante dans Root).
  • 0x80092004 (CRYPT_E_NOT_FOUND) : magasin non initialisé ou fichier .cer invalide.
  • 0x80090008 (NTE_BAD_ALGID) : algorithme non supporté sur un OS ancien/limité.

Points clés à retenir

  • L’erreur E_ACCESSDENIED lors d’un Import-Certificate dans TrustedPublisher sur une VM Hyper‑V neuve est le plus souvent liée au magasin non matérialisé et/ou au filtrage UAC en session distante.
  • La pré‑création du magasin + un repli certutil règle l’écrasante majorité des cas sans modifier la posture sécurité.
  • Pour l’industrialisation, préférez GPO ou une tâche planifiée SYSTEM.
  • Toujours vérifier par empreinte après import et journaliser l’opération.

Note : dans plusieurs échanges communautaires, aucune solution technique n’a été fournie et des redirections « génériques » ont été proposées. Cet article rassemble les causes connues et des procédures concrètes pour corriger l’erreur de manière reproductible.

Sommaire