From 4c4f900ba38a31c2c9f06b755a655ae4a8ba5c5f Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 24 Apr 2026 13:21:16 -0500 Subject: [PATCH 01/53] Initial JEA for WinCert --- .vscode/launch.json | 15 ++ CHANGELOG.md | 4 + IISU/ImplementedStoreTypes/Win/Inventory.cs | 9 +- IISU/ImplementedStoreTypes/Win/Management.cs | 12 +- .../Win/WinCertCertificateInfo.cs | 1 - .../ImplementedStoreTypes/Win/WinInventory.cs | 1 + .../WinADFS/Inventory.cs | 3 +- .../ImplementedStoreTypes/WinIIS/Inventory.cs | 4 +- .../WinIIS/Management.cs | 4 +- .../ImplementedStoreTypes/WinSQL/Inventory.cs | 4 +- .../WinSQL/Management.cs | 6 +- IISU/Models/JobProperties.cs | 8 + IISU/PSHelper.cs | 153 +++++++++++++----- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 90 +++++++++++ .../Keyfactor.WinCert.Common.psm1 | 27 ++++ .../Private/Get-CertificateCSP.ps1 | 89 ++++++++++ .../Private/Get-CertificateSAN.ps1 | 8 + .../Private/Get-CryptoProviders.ps1 | 27 ++++ .../Private/Test-CryptoServiceProvider.ps1 | 14 ++ .../Private/Validate-CryptoProvider.ps1 | 16 ++ .../Public/Add-KeyfactorCertificate.ps1 | 128 +++++++++++++++ .../Public/Get-KeyfactorCertificates.ps1 | 71 ++++++++ .../Public/New-KeyfactorResult.ps1 | 49 ++++++ .../Public/Remove-KeyfactorCertificate.ps1 | 52 ++++++ .../Keyfactor.WinCert.Common.psrc | 65 ++++++++ .../WinADFSScripts.ps1 | 0 .../WinCertScripts.ps1 | 9 ++ IISU/RemoteSettings.cs | 2 + IISU/WindowsCertStore.csproj | 19 ++- 29 files changed, 830 insertions(+), 60 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 IISU/PowerShell/Build/KeyfactorWinCert.pssc create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateSAN.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-CryptoServiceProvider.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorCertificates.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/Remove-KeyfactorCertificate.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc rename IISU/{PowerShellScripts => PowerShell}/WinADFSScripts.ps1 (100%) rename IISU/{PowerShellScripts => PowerShell}/WinCertScripts.ps1 (99%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..60db609b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "PowerShell: Launch Script", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "args": [] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ce49d57b..632a987b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +4.0.0 +* As of this version of the extension, SANs will be handled through the ODKG Enrollment page in Command and will no longer use the SAN Entry Parameter. This version, we are removing all support for the SAN Entry Parameter. If you are still using the SAN Entry Parameter, you will need to remove it from your store types and re-run inventory to remove it from your database. +* Adding JEA Support for local PowerShell execution. This will allow for more secure execution of the extension when running in a local PowerShell Runspace. To utilize this feature, you will need to create a JEA endpoint on the target server and specify the endpoint name as a new parameter in the specific Cert Store definition. Refer to the README for more details. + 3.0.1 * Fixed an issues when renewing ECC Certificates diff --git a/IISU/ImplementedStoreTypes/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs index 546640b8..d5a8f934 100644 --- a/IISU/ImplementedStoreTypes/Win/Inventory.cs +++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs @@ -81,6 +81,8 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; + settings.UseJEA = jobProperties.UseJEA; + settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace($"Querying Window certificate in store: {storePath}"); inventoryItems = QueryWinCertCertificates(settings, storePath); @@ -126,7 +128,7 @@ public List QueryWinCertCertificates(RemoteSettings settin { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); @@ -135,7 +137,7 @@ public List QueryWinCertCertificates(RemoteSettings settin { "StoreName", StoreName } }; - results = ps.ExecutePowerShell("Get-KFCertificates", parameters); + results = ps.ExecutePowerShell("Get-KefactorCertificates", parameters); // If there are certificates, deserialize the results and send them back to command if (results != null && results.Count > 0) @@ -147,8 +149,7 @@ public List QueryWinCertCertificates(RemoteSettings settin { var siteSettingsDict = new Dictionary { - { "ProviderName", cert.ProviderName}, - { "SAN", cert.SAN } + { "ProviderName", cert.ProviderName} }; Inventory.Add( diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index bbb860f9..672f2b91 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -88,8 +88,10 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; + bool useJea = jobProperties?.UseJEA ?? false; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); switch (_operationType) { @@ -158,8 +160,8 @@ public JobResult AddCertificate(string certificateContents, string privateKeyPas if (!string.IsNullOrEmpty(privateKeyPassword)) { parameters.Add("PrivateKeyPassword", privateKeyPassword); } if (!string.IsNullOrEmpty(cryptoProvider)) { parameters.Add("CryptoServiceProvider", cryptoProvider); } - _results = _psHelper.ExecutePowerShell("Add-KFCertificateToStore", parameters); - _logger.LogTrace("Returned from executing PS function (Add-KFCertificateToStore)"); + _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); + _logger.LogTrace("Returned from executing PS function (Add-KeyfactorCertificate)"); // This should return the thumbprint of the certificate if (_results != null && _results.Count > 0) @@ -212,8 +214,8 @@ public JobResult RemoveCertificate(string thumbprint) { "StorePath", _storePath } }; - _psHelper.ExecutePowerShell("Remove-KFCertificateFromStore", parameters); - _logger.LogTrace("Returned from executing PS function (Remove-KFCertificateFromStore)"); + _psHelper.ExecutePowerShell("Remove-KeyfactorCertificate", parameters); + _logger.LogTrace("Returned from executing PS function (Remove-KeyfactorCertificate)"); _psHelper.Terminate(); } diff --git a/IISU/ImplementedStoreTypes/Win/WinCertCertificateInfo.cs b/IISU/ImplementedStoreTypes/Win/WinCertCertificateInfo.cs index 27d5e00e..7dfafa6e 100644 --- a/IISU/ImplementedStoreTypes/Win/WinCertCertificateInfo.cs +++ b/IISU/ImplementedStoreTypes/Win/WinCertCertificateInfo.cs @@ -24,7 +24,6 @@ public class WinCertCertificateInfo public string Issuer { get; set; } public string Thumbprint { get; set; } public bool HasPrivateKey { get; set; } - public string SAN { get; set; } public string ProviderName { get; set; } public string Base64Data { get; set; } } diff --git a/IISU/ImplementedStoreTypes/Win/WinInventory.cs b/IISU/ImplementedStoreTypes/Win/WinInventory.cs index 0cae7b8c..a60e067a 100644 --- a/IISU/ImplementedStoreTypes/Win/WinInventory.cs +++ b/IISU/ImplementedStoreTypes/Win/WinInventory.cs @@ -24,6 +24,7 @@ namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinCert { + [Obsolete("This class is no longer used and will be removed in a future release.")] internal class WinInventory : ClientPSCertStoreInventory { private ILogger _logger; diff --git a/IISU/ImplementedStoreTypes/WinADFS/Inventory.cs b/IISU/ImplementedStoreTypes/WinADFS/Inventory.cs index 639335f2..ffb8e416 100644 --- a/IISU/ImplementedStoreTypes/WinADFS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinADFS/Inventory.cs @@ -180,8 +180,7 @@ public List QueryWinADFSCertificates(RemoteSettings settin { var siteSettingsDict = new Dictionary { - { "ProviderName", cert.ProviderName}, - { "SAN", cert.SAN } + { "ProviderName", cert.ProviderName} }; Inventory.Add( diff --git a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs index 65c1b68c..390c678a 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs @@ -83,6 +83,8 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; + settings.UseJEA = jobProperties.UseJEA; + settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace("Querying IIS Inventory.."); inventoryItems = QueryIISCertificates(settings); @@ -127,7 +129,7 @@ public List QueryIISCertificates(RemoteSettings settings) { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index b458584d..2247431b 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -92,12 +92,14 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; + bool useJea = jobProperties?.UseJEA ?? false; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; string alias = config.JobCertificate?.Alias?.Split(':').FirstOrDefault() ?? string.Empty; // Thumbprint is first part of the alias // Assign the binding information IISBindingInfo bindingInfo = new IISBindingInfo(config.JobProperties); - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); _psHelper.Initialize(); diff --git a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs index 765cf0a7..4e1f3fff 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs @@ -81,6 +81,8 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; + settings.UseJEA = jobProperties.UseJEA; + settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace($"Attempting to read bound SQL Server certificates from cert store: {storePath}"); inventoryItems = QuerySQLCertificates(settings, storePath); @@ -125,7 +127,7 @@ public List QuerySQLCertificates(RemoteSettings settings, { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index 2499dc5e..61be06d3 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -91,7 +91,9 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; - + bool useJea = jobProperties?.UseJEA ?? false; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; + RestartSQLService = jobProperties.RestartService; if (config.JobProperties.ContainsKey("InstanceName")) @@ -104,7 +106,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) RenewalThumbprint = config.JobProperties["RenewalThumbprint"]?.ToString(); } - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); _psHelper.Initialize(); diff --git a/IISU/Models/JobProperties.cs b/IISU/Models/JobProperties.cs index a6ef63b0..3d534e78 100644 --- a/IISU/Models/JobProperties.cs +++ b/IISU/Models/JobProperties.cs @@ -51,5 +51,13 @@ public JobProperties() [JsonProperty("RestartService")] [DefaultValue(true)] public bool RestartService { get; set; } + + [JsonProperty("UseJEA")] + [DefaultValue(false)] + public bool UseJEA { get; set; } + + [JsonProperty("JEAEndpointName")] + [DefaultValue("keyfactor.wincert")] + public string JEAEndpointName { get; set; } = "keyfactor.wincert"; } } \ No newline at end of file diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 31b73af9..b4c1a77c 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -60,6 +60,8 @@ public class PSHelper : IDisposable private bool isLocalMachine; private bool isADFSStore = false; + private bool useJea = false; + private string jeaEndpoint = "keyfactor.wincert"; public bool IsLocalMachine { @@ -95,7 +97,7 @@ public PSHelper() _logger = LogHandler.GetClassLogger(); } - public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false) + public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false, bool useJea = false, string jeaEndpoint = "keyfactor.wincert") { this.protocol = protocol.ToLower(); this.port = port; @@ -103,6 +105,9 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN ClientMachineName = clientMachineName; this.serverUserName = serverUserName; this.serverPassword = serverPassword; + this.isADFSStore = isADFSStore; + this.useJea = useJea; + this.jeaEndpoint = jeaEndpoint; _logger = LogHandler.GetClassLogger(); _logger.LogTrace("Entered PSHelper Constructor"); @@ -110,8 +115,9 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN _logger.LogTrace($"Port: {this.port}"); _logger.LogTrace($"UseSPN: {this.useSPN}"); _logger.LogTrace($"ClientMachineName: {ClientMachineName}"); + _logger.LogTrace($"UseJEA: {this.useJea}"); + _logger.LogTrace($"JEAEndpoint: {this.jeaEndpoint}"); _logger.LogTrace("Constructor Completed"); - this.isADFSStore = isADFSStore; } public void Initialize() @@ -132,8 +138,8 @@ public void Initialize() _logger.LogDebug($"isLocalMachine flag set to: {isLocalMachine}"); _logger.LogDebug($"Protocol is set to: {protocol}"); - scriptFileLocation = FindScriptsDirectory(AppDomain.CurrentDomain.BaseDirectory, "PowerShellScripts"); - if (scriptFileLocation == null) { throw new Exception("Unable to find the accompanying PowerShell Script files,"); } + scriptFileLocation = FindScriptsDirectory(AppDomain.CurrentDomain.BaseDirectory, "PowerShell"); + if (scriptFileLocation == null) { throw new Exception("Unable to find the accompanying PowerShell Script files."); } _logger.LogTrace($"Script file located here: {scriptFileLocation}"); @@ -168,6 +174,7 @@ public void Initialize() private void InitializeRemoteSession() { if (this.isADFSStore) throw new Exception("Remote ADFS stores are not supported."); + if (this.useJea && protocol == "ssh") throw new Exception("JEA is not supported over SSH. Use WinRM (http/https) for JEA."); if (protocol == "ssh") { @@ -213,6 +220,12 @@ private void InitializeRemoteSession() .AddParameter("Port", port) .AddParameter("SessionOption", sessionOption); + if (useJea) + { + PS.AddParameter("ConfigurationName", jeaEndpoint); + _logger.LogDebug($"JEA enabled - connecting to endpoint: {jeaEndpoint}"); + } + if (protocol == "https") { _logger.LogTrace($"Using HTTPS to connect to: {clientMachineName}"); @@ -246,13 +259,20 @@ private void InitializeRemoteSession() PS.Commands.Clear(); _logger.LogTrace("PS-Session established"); - PS.AddCommand("Invoke-Command") - .AddParameter("Session", _PSSession) - .AddParameter("ScriptBlock", ScriptBlock.Create(LoadAllScripts(scriptFileLocation))); + if (!useJea) + { + PS.AddCommand("Invoke-Command") + .AddParameter("Session", _PSSession) + .AddParameter("ScriptBlock", ScriptBlock.Create(LoadAllScripts(scriptFileLocation))); - var results = PS.Invoke(); - CheckErrors(); - _logger.LogTrace("Script loaded into remote session successfully."); + PS.Invoke(); + CheckErrors(); + _logger.LogTrace("Scripts loaded into remote session successfully."); + } + else + { + _logger.LogDebug($"JEA session active on endpoint '{jeaEndpoint}' - skipping script injection, functions are pre-registered."); + } } else { @@ -280,40 +300,53 @@ private void InitializeLocalSession() adfsModuleImported = ImportAdfsModule(); } - // Load all scripts + // Import the Keyfactor.WinCert.Common module (handles Private-before-Public load order internally) _logger.LogTrace("Loading PowerShell scripts"); - var scriptFiles = GetScriptFiles(scriptFileLocation); - - foreach (var scriptFile in scriptFiles) + string moduleFile = Path.Combine(scriptFileLocation, "Keyfactor.WinCert.Common", "Keyfactor.WinCert.Common.psm1"); + if (File.Exists(moduleFile)) { - var fileName = Path.GetFileName(scriptFile); - bool isAdfsScript = fileName.IndexOf("adfs", StringComparison.OrdinalIgnoreCase) >= 0; + _logger.LogTrace($"Importing module: {moduleFile}"); + PS.AddCommand("Import-Module") + .AddParameter("Name", moduleFile) + .AddParameter("Force"); + PS.Invoke(); - // Decide whether to load this script - if (isAdfsScript) + if (PS.HadErrors) { - if (this.isADFSStore) - { - if (!adfsModuleImported) - { - _logger.LogWarning($"Skipping ADFS script '{fileName}' - ADFS module not available"); - continue; - } - - _logger.LogTrace($"Loading ADFS script: {fileName}"); - } - else - { - _logger.LogTrace($"Skipping ADFS script '{fileName}' - not an ADFS store"); - continue; - } + _logger.LogError("Errors importing Keyfactor.WinCert.Common module:"); + foreach (var error in PS.Streams.Error) + _logger.LogError($" {error}"); + PS.Streams.Error.Clear(); } else { - _logger.LogTrace($"Loading script: {fileName}"); + _logger.LogInformation("Keyfactor.WinCert.Common module imported successfully."); } + PS.Commands.Clear(); + } + else + { + _logger.LogWarning($"Keyfactor.WinCert.Common module not found at: {moduleFile}"); + } - // Load the script + // Load flat legacy .ps1 scripts from the PowerShell root directory + var rootScripts = Directory.GetFiles(scriptFileLocation, "*.ps1") + .Where(f => !f.EndsWith(".example", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f) + .ToList(); + + foreach (var scriptFile in rootScripts) + { + var fileName = Path.GetFileName(scriptFile); + bool isAdfsScript = fileName.IndexOf("adfs", StringComparison.OrdinalIgnoreCase) >= 0; + + if (isAdfsScript && (!this.isADFSStore || !adfsModuleImported)) + { + _logger.LogTrace($"Skipping ADFS script '{fileName}'"); + continue; + } + + _logger.LogTrace($"Loading script: {fileName}"); try { PS.AddScript($". '{scriptFile}'"); @@ -323,16 +356,14 @@ private void InitializeLocalSession() { _logger.LogError($"Errors loading script '{fileName}':"); foreach (var error in PS.Streams.Error) - { _logger.LogError($" {error}"); - } + PS.Streams.Error.Clear(); } else { - _logger.LogTrace($" ✓ Successfully loaded {fileName}"); + _logger.LogTrace($" Successfully loaded {fileName}"); } - CheckErrors(); PS.Commands.Clear(); } catch (Exception ex) @@ -961,6 +992,46 @@ private static string formatPrivateKey(string privateKey) return privateKey.Replace(header, "HEADER").Replace(footer, "FOOTER").Replace(" ", Environment.NewLine).Replace("HEADER", header).Replace("FOOTER", footer) + Environment.NewLine; } + private static List GetOrderedScriptFiles(string scriptsDirectory) + { + /* + * Returns .ps1 files in dependency order for remote non-JEA sessions: + * 1. Flat .ps1 files in the root PowerShell directory (legacy scripts) + * 2. For each module subdirectory: Private/*.ps1 first, then Public/*.ps1 + */ + var ordered = new List(); + + // Root flat scripts (WinCertScripts.ps1, WinADFSScripts.ps1, etc.) + ordered.AddRange( + Directory.GetFiles(scriptsDirectory, "*.ps1") + .Where(f => !f.EndsWith(".example", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f)); + + // Module subdirectories: Private before Public so helpers are defined first + foreach (var subDir in Directory.GetDirectories(scriptsDirectory).OrderBy(d => d)) + { + var privatePath = Path.Combine(subDir, "Private"); + if (Directory.Exists(privatePath)) + { + ordered.AddRange( + Directory.GetFiles(privatePath, "*.ps1", SearchOption.AllDirectories) + .Where(f => !f.EndsWith(".example", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f)); + } + + var publicPath = Path.Combine(subDir, "Public"); + if (Directory.Exists(publicPath)) + { + ordered.AddRange( + Directory.GetFiles(publicPath, "*.ps1", SearchOption.AllDirectories) + .Where(f => !f.EndsWith(".example", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f)); + } + } + + return ordered; + } + public static string FindScriptsDirectory(string rootDirectory, string directoryName) { /* @@ -1094,8 +1165,8 @@ public string LoadAllScripts(string scriptFileLocation) _logger.LogInformation($"Loading scripts from: {scriptsDirectory}"); - // Load all .ps1 files from the scripts directory - var scriptFiles = Directory.GetFiles(scriptsDirectory, "*.ps1").ToList(); + // Load scripts in dependency order: root files first, then module Private then Public + var scriptFiles = GetOrderedScriptFiles(scriptsDirectory); if (scriptFiles.Count == 0) { diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc new file mode 100644 index 00000000..8e225248 --- /dev/null +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -0,0 +1,90 @@ +# +# KeyfactorWinCert.pssc +# JEA Session Configuration file for Keyfactor Windows Certificate Store management +# +# ============================================================ +# PREREQUISITES (run once per target machine as Administrator) +# ============================================================ +# +# 1. Install the module to the system module path so it is treated as trusted code: +# +# $dest = 'C:\Program Files\WindowsPowerShell\Modules\Keyfactor.WinCert.Common' +# Copy-Item -Path '.\Keyfactor.WinCert.Common' -Destination $dest -Recurse -Force +# +# 2. Create the transcript directory for audit logging: +# +# New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force +# +# 3. Register this session configuration (required once; re-run after any change): +# +# Register-PSSessionConfiguration ` +# -Name 'keyfactor.wincert' ` +# -Path 'C:\path\to\KeyfactorWinCert.pssc' ` +# -Force +# Restart-Service WinRM +# +# 4. Verify the endpoint is registered: +# +# Get-PSSessionConfiguration | Where-Object Name -eq 'keyfactor.wincert' +# +# 5. To update or remove the endpoint: +# +# Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' +# Restart-Service WinRM +# +# ============================================================ +# TESTING THE ENDPOINT +# ============================================================ +# +# # Connect and list available commands (should show only the 4 Keyfactor functions) +# $s = New-PSSession -ComputerName ` +# -ConfigurationName 'keyfactor.wincert' ` +# -Credential (Get-Credential) +# Invoke-Command -Session $s -ScriptBlock { Get-Command } +# +# # Run an inventory +# Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } +# +# Remove-PSSession $s +# +# ============================================================ +@{ + SchemaVersion = '2.0.0.0' + GUID = '4d1866a4-858a-4c60-866c-b46f546dc014' + Author = 'Keyfactor' + Description = 'JEA session configuration for Keyfactor Windows Certificate Store management' + + # RestrictedRemoteServer limits the session to only what is explicitly permitted. + SessionType = 'RestrictedRemoteServer' + + # ConstrainedLanguage allows param() blocks and variable usage in caller scriptblocks + # (required for PSHelper's parameter-passing pattern). Module functions installed under + # C:\Program Files\WindowsPowerShell\Modules\ run as FULLY TRUSTED regardless of this setting. + LanguageMode = 'ConstrainedLanguage' + + # --- Run-As Account --- + # For development/testing: RunAsVirtualAccount creates a temporary local admin account. + # For production: comment out RunAsVirtualAccount and use a Group Managed Service Account + # that has been granted only the rights needed (read/write the certificate store). + # + # Production example: + # GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' + # + RunAsVirtualAccount = $true + + # Transcript every session to this directory for audit purposes. + # Directory must exist before registering -- see Prerequisites step 2 above. + TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' + + # --- Role Definitions --- + # Map the connecting user/group to the Keyfactor.WinCert.Common role capability. + # Replace 'BUILTIN\Administrators' with the AD group or local group whose members + # are authorized to connect (e.g. 'DOMAIN\KeyfactorOrchestrators'). + # + # Multiple groups can be granted the same or different roles: + # 'DOMAIN\CertAdmins' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } + # 'DOMAIN\CertReadOnly' = @{ RoleCapabilities = 'Keyfactor.WinCert.ReadOnly' } + RoleDefinitions = @{ + 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } + } +} diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 new file mode 100644 index 00000000..9cfdaa96 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 @@ -0,0 +1,27 @@ +# Keyfactor.WinCert.Common.psm1 +# +# Static explicit dot-sourcing is used instead of Get-ChildItem/Select-Object discovery. +# This avoids a dependency on proxy-restricted cmdlets during JEA session initialization. +# (In RestrictedRemoteServer, Select-Object -ExpandProperty is not available in the proxy.) + +# Private helpers must be loaded before the public functions that call them. +. "$PSScriptRoot\Private\Get-CertificateSAN.ps1" +. "$PSScriptRoot\Private\Get-CertificateCSP.ps1" +. "$PSScriptRoot\Private\Get-CryptoProviders.ps1" +. "$PSScriptRoot\Private\Test-CryptoServiceProvider.ps1" +. "$PSScriptRoot\Private\Validate-CryptoProvider.ps1" + +# Public functions +. "$PSScriptRoot\Public\New-KeyfactorResult.ps1" +. "$PSScriptRoot\Public\Get-KeyfactorCertificates.ps1" +. "$PSScriptRoot\Public\Add-KeyfactorCertificate.ps1" +. "$PSScriptRoot\Public\Remove-KeyfactorCertificate.ps1" + +# Export only public functions for non-JEA use. +# In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. +Export-ModuleMember -Function @( + 'New-KeyfactorResult', + 'Get-KeyfactorCertificates', + 'Add-KeyfactorCertificate', + 'Remove-KeyfactorCertificate' +) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 new file mode 100644 index 00000000..e10ba5bc --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 @@ -0,0 +1,89 @@ +function Get-CertificateCSP +{ + param( + [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert + ) + + # Helper: extract KSP/provider name from a CNG key object + function Get-CngProviderName { + param($key) + try { + # RSACng / ECDsaCng expose a .Key property (CngKey) + if ($key.PSObject.Properties['Key']) { + $cngKey = $key.Key + if ($cngKey -and $cngKey.Provider -and $cngKey.Provider.Provider) { + return [string]$cngKey.Provider.Provider + } + } + } + catch { + Write-Verbose "CNG provider lookup failed: $($_.Exception.Message)" + } + return $null + } + + try { + if (-not $cert.HasPrivateKey) { + return "No private key" + } + + # ── 1. Legacy CryptoAPI path (RSACryptoServiceProvider) ────────────── + $privateKey = $cert.PrivateKey + if ($privateKey -and $privateKey.CspKeyContainerInfo) { + $providerName = $privateKey.CspKeyContainerInfo.ProviderName + if ($providerName) { + return [string]$providerName + } + } + + # ── 2. CNG RSA (RSACng) ─────────────────────────────────────────────── + try { + $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) + if ($rsaKey) { + $providerName = Get-CngProviderName $rsaKey + if ($providerName) { return $providerName } + } + } + catch { + Write-Verbose "RSA CNG detection failed: $($_.Exception.Message)" + } + + # ── 3. ECC / ECDsa (ECDsaCng) ───────────────────────────────────────── + # ECC keys always use CNG (KSPs), never legacy CSPs + try { + $ecKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($cert) + if ($ecKey) { + $providerName = Get-CngProviderName $ecKey + if ($providerName) { return $providerName } + + Write-Verbose "ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" + return "" + } + } + catch { + Write-Verbose "ECDsa CNG detection failed: $($_.Exception.Message)" + } + + # ── 4. DSA (bonus) ──────────────────────────────────────────────────── + try { + $dsaKey = [System.Security.Cryptography.X509Certificates.DSACertificateExtensions]::GetDSAPrivateKey($cert) + if ($dsaKey) { + $providerName = Get-CngProviderName $dsaKey + if ($providerName) { return $providerName } + + Write-Verbose "DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" + return "" + } + } + catch { + Write-Verbose "DSA CNG detection failed: $($_.Exception.Message)" + } + + Write-Verbose "No supported key type detected; provider name could not be determined" + return "" + } + catch { + Write-Warning "Error retrieving CSP for certificate '$($cert.Subject)': $($_.Exception.Message)" + return "" + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateSAN.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateSAN.ps1 new file mode 100644 index 00000000..3e6b3e8f --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateSAN.ps1 @@ -0,0 +1,8 @@ +function Get-CertificateSAN($cert) +{ + $san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" } + if ($san) { + return ($san.Format(1) -split ", " -join "; ") + } + return $null +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 new file mode 100644 index 00000000..b8257337 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 @@ -0,0 +1,27 @@ +function Get-CryptoProviders { + # Retrieves the list of available Crypto Service Providers using certutil + try { + Write-Verbose "Retrieving Crypto Service Providers using certutil..." + $certUtilOutput = certutil -csplist + + # Parse the output to extract CSP names + $cspInfoList = @() + foreach ($line in $certUtilOutput) { + if ($line -match "Provider Name:") { + $cspName = ($line -split ":")[1].Trim() + $cspInfoList += $cspName + } + } + + if ($cspInfoList.Count -eq 0) { + throw "No Crypto Service Providers were found. Ensure certutil is functioning properly." + } + + Write-Verbose "Retrieved the following CSPs:" + $cspInfoList | ForEach-Object { Write-Verbose $_ } + + return $cspInfoList + } catch { + throw "Failed to retrieve Crypto Service Providers: $_" + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-CryptoServiceProvider.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-CryptoServiceProvider.ps1 new file mode 100644 index 00000000..bc28a329 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-CryptoServiceProvider.ps1 @@ -0,0 +1,14 @@ +function Test-CryptoServiceProvider { + param( + [Parameter(Mandatory = $true)] + [string]$CSPName + ) + + try { + Validate-CryptoProvider -ProviderName $CSPName -Verbose:$false + return $true + } + catch { + return $false + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 new file mode 100644 index 00000000..4991205e --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 @@ -0,0 +1,16 @@ +function Validate-CryptoProvider { + param ( + [Parameter(Mandatory)] + [string]$ProviderName + ) + Write-Verbose "Validating CSP: $ProviderName" + + $availableProviders = Get-CryptoProviders + + if (-not ($availableProviders | Where-Object { $_.Trim().ToLowerInvariant() -eq $ProviderName.Trim().ToLowerInvariant() })) { + + throw "Crypto Service Provider '$ProviderName' is either invalid or not found on this system." + } + + Write-Verbose "Crypto Service Provider '$ProviderName' is valid." +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 new file mode 100644 index 00000000..ed44b4f0 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 @@ -0,0 +1,128 @@ +function Add-KeyfactorCertificate { + param ( + [Parameter(Mandatory = $true)] + [string]$Base64Cert, + + [Parameter(Mandatory = $false)] + [string]$PrivateKeyPassword, + + [Parameter(Mandatory = $true)] + [string]$StoreName, + + [Parameter(Mandatory = $false)] + [string]$CryptoServiceProvider + ) + + try { + Write-Information "Entering PowerShell Script Add-KeyfactorCertificateToStore" + Write-Verbose "Add-KeyfactorCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" + + # Get the thumbprint of the passed in certificate + # Convert password to secure string if provided, otherwise use $null + $bytes = [System.Convert]::FromBase64String($Base64Cert) + $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } + + # Set the storage flags and get the certificate's thumbprint + $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor ` + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet + + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, $keyStorageFlags) + $thumbprint = $cert.Thumbprint + + if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } + + if ($CryptoServiceProvider) + { + # Test to see if CSP exists + if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) + { + Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system." + Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system." + return + } + + Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" + + # Create temporary file for the PFX + $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx" + [System.IO.File]::WriteAllBytes($tempPfx, [Convert]::FromBase64String($Base64Cert)) + + + # Execute certutil based on whether a private key password was supplied + try { + # Start building certutil arguments + $arguments = @('-f') + + if ($PrivateKeyPassword) { + Write-Verbose "Has a private key" + $arguments += '-p' + $arguments += $PrivateKeyPassword + } + + if ($CryptoServiceProvider) { + Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider" + $arguments += '-csp' + $arguments += $CryptoServiceProvider + } + + $arguments += '-importpfx' + $arguments += $StoreName + $arguments += $tempPfx + + # Quote any arguments with spaces + $argLine = ($arguments | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' + + write-Verbose "Running certutil with arguments: $argLine" + + # Setup process execution + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "certutil.exe" + $processInfo.Arguments = $argLine.Trim() + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + + $process.Start() | Out-Null + + $stdOut = $process.StandardOutput.ReadToEnd() + $stdErr = $process.StandardError.ReadToEnd() + + $process.WaitForExit() + + if ($process.ExitCode -ne 0) { + throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" + } + } catch { + Write-Error "ERROR: $_" + } finally { + if (Test-Path $tempPfx) { + Remove-Item $tempPfx -Force + } + } + + } else { + $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" + Write-Information "Store '$StoreName' is open." + + # Open store with read/write, and don't create the store if it doesn't exist + $openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite -bor ` + [System.Security.Cryptography.X509Certificates.OpenFlags]::OpenExistingOnly + $certStore.Open($openFlags) + $certStore.Add($cert) + $certStore.Close(); + Write-Information "Store '$StoreName' is closed." + } + + Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." + return $thumbprint + } catch { + Write-Error "An error occurred: $_" + return $null + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorCertificates.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorCertificates.ps1 new file mode 100644 index 00000000..f483ae58 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorCertificates.ps1 @@ -0,0 +1,71 @@ +function Get-KeyfactorCertificates { + param ( + [Parameter(Mandatory = $false)] + [string]$StoreName = "My", # Default store name is "My" (Personal) + + [Parameter(Mandatory = $false)] + [string]$Thumbprint # Optional: specific certificate thumbprint to retrieve + ) + + # Define the store path using the provided StoreName parameter + $storePath = "Cert:\LocalMachine\$StoreName" + + # Check if the store path exists to ensure the store is valid + if (-not (Test-Path $storePath)) { + # Write an error message and exit the function if the store path is invalid + Write-Error "The certificate store path '$storePath' does not exist. Please provide a valid store name." + return + } + + # Retrieve certificates from the specified store + if ($Thumbprint) { + # If thumbprint is provided, retrieve only that specific certificate + # Remove any spaces or special characters from the thumbprint for comparison + $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' + $certificates = Get-ChildItem -Path $storePath | Where-Object { + ($_.Thumbprint -replace '[^a-fA-F0-9]', '') -eq $cleanThumbprint + } + + if (-not $certificates) { + Write-Error "No certificate found with thumbprint '$Thumbprint' in store '$StoreName'." + return + } + } else { + # Retrieve all certificates from the specified store + $certificates = Get-ChildItem -Path $storePath + } + + # Initialize an empty array to store certificate information objects + $certInfoList = @() + + foreach ($cert in $certificates) { + try { + # Create a custom object to store details about the current certificate + $certInfo = [PSCustomObject]@{ + StoreName = $StoreName # Name of the certificate store + Certificate = $cert.Subject # Subject of the certificate + ExpiryDate = $cert.NotAfter # Expiration date of the certificate + Issuer = $cert.Issuer # Issuer of the certificate + Thumbprint = $cert.Thumbprint # Unique thumbprint of the certificate + HasPrivateKey = $cert.HasPrivateKey # Indicates if the certificate has a private key + SAN = Get-CertificateSAN $cert # Subject Alternative Names (if available) + ProviderName = Get-CertificateCSP $cert # Provider of the certificate + Base64Data = [System.Convert]::ToBase64String($cert.RawData) # Encoded raw certificate data + } + + # Add the certificate information object to the results array + $certInfoList += $certInfo + } catch { + # Write a warning message if there is an error processing the current certificate + Write-Warning "An error occurred while processing the certificate: $_" + } + } + + # Output the results in JSON format if certificates were found + if ($certInfoList) { + $certInfoList | ConvertTo-Json -Depth 10 + } else { + # Write a warning if no certificates were found in the specified store + Write-Warning "No certificates were found in the store '$StoreName'." + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 new file mode 100644 index 00000000..9bfec511 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 @@ -0,0 +1,49 @@ +#Standard Step Names +# Step Name Purpose +# ValidateInput Validate required params and input data +# FindSite Checking if the IIS site exists +# CheckBinding Looking up existing bindings +# RemoveBinding Attempting to remove an old binding +# AddBinding Adding the new IIS binding +# LoadCertificate Fetching or validating the SSL certificate +# CompareThumbprint Checking if binding needs to be updated +# BindSSL Adding SSL cert to a binding +# ImportModules Importing IIS-related PowerShell modules +# CatchAll Fallback for unexpected or generic errors + +# Standard Error Codes +#Code Status Description +# 0 Success Operation completed successfully +# 100 Skipped Binding already exists and is up-to-date +# 101 Warning Binding exists but is invalid +# 200 Error Site not found +# 201 Error Failed to remove binding +# 202 Error Failed to add binding +# 203 Error Certificate not found +# 204 Error Certificate already in use elsewhere +# 205 Error Thumbprint mismatch +# 206 Error WebAdministration module missing +# 207 Error IISAdministration module missing +# 300 Error Unknown or unhandled exception +# 400 Error Invalid Ssl Flag bit combination + +function New-KeyfactorResult { + param( + [ValidateSet("Success", "Warning", "Error", "Skipped")] + [string]$Status, + [int]$Code, + [string]$Step, + [string]$Message, + [string]$ErrorMessage = "", + [hashtable]$Details = @{} + ) + + return [PSCustomObject]@{ + Status = $Status + Code = $Code + Step = $Step + Message = $Message + ErrorMessage = $ErrorMessage + Details = $Details + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Remove-KeyfactorCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Remove-KeyfactorCertificate.ps1 new file mode 100644 index 00000000..a6267f3b --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Remove-KeyfactorCertificate.ps1 @@ -0,0 +1,52 @@ +function Remove-KeyfactorCertificate { + param ( + [string]$Thumbprint, + [string]$StorePath, + + [parameter(ParameterSetName = $false)] + [switch]$IsAlias + ) + + # Initialize a variable to track success + $isSuccessful = $false + + try { + # Open the certificate store + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($StorePath, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + + # Find the certificate by thumbprint or alias + if ($IsAlias) { + $cert = $store.Certificates | Where-Object { $_.FriendlyName -eq $Thumbprint } + } else { + $cert = $store.Certificates | Where-Object { $_.Thumbprint -eq $Thumbprint } + } + + if ($cert) { + # Remove the certificate from the store + Write-Information "Attempting to remove certificate from store '$StorePath' with the thumbprint: $Thumbprint" + $store.Remove($cert) + Write-Information "Certificate removed successfully from store '$StorePath'" + + # Mark success + $isSuccessful = $true + } else { + throw [System.Exception]::new("Certificate not found in $StorePath.") + } + + # Close the store + $store.Close() + } catch { + # Log and rethrow the exception + Write-Error "An error occurred: $_" + throw $_ + } finally { + # Ensure the store is closed + if ($store) { + $store.Close() + } + } + + # Return the success status + return $isSuccessful +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc new file mode 100644 index 00000000..99d8e2a7 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc @@ -0,0 +1,65 @@ +# +# Keyfactor.WinCert.Common.psrc +# JEA Role Capability file for Keyfactor Windows Certificate Store (Common operations) +# +# INSTALL LOCATION: +# This file must live inside the module folder that PowerShell can discover: +# C:\Program Files\WindowsPowerShell\Modules\ +# Keyfactor.WinCert.Common\ +# RoleCapabilities\ +# Keyfactor.WinCert.Common.psrc <-- this file +# +# PowerShell finds the role capability by name ('Keyfactor.WinCert.Common') by searching +# all modules in $env:PSModulePath for a RoleCapabilities subfolder with a matching .psrc. +# +# LANGUAGE MODE NOTE: +# The session configuration (KeyfactorWinCert.pssc) sets LanguageMode = 'ConstrainedLanguage'. +# Module code loaded from a trusted path ($env:PSModulePath) runs as FULLY TRUSTED and can +# use .NET APIs freely. The ConstrainedLanguage restriction only applies to caller scriptblocks +# sent by the orchestrator (e.g., param() blocks for parameter passing). +# +@{ + GUID = 'c068f073-354c-4a88-a325-23d1e778334d' + Author = 'Keyfactor' + Description = 'Role capabilities for Keyfactor Windows Certificate Store common operations (inventory, add, remove)' + + # Import the module so its functions are pre-loaded and run as trusted code. + # The module MUST be installed under C:\Program Files\WindowsPowerShell\Modules\ so that + # PowerShell treats it as trusted (required for .NET API usage in ConstrainedLanguage sessions). + ModulesToImport = @('Keyfactor.WinCert.Common') + + # Functions the orchestrator is allowed to invoke in this JEA session. + # Private helper functions (Get-CertificateCSP, Get-CertificateSAN, etc.) are intentionally + # excluded -- they are called internally by the public functions, not by the orchestrator. + VisibleFunctions = @( + 'Get-KeyfactorCertificates', + 'Add-KeyfactorCertificate', + 'Remove-KeyfactorCertificate', + 'New-KeyfactorResult' + ) + + # Cmdlets available to caller scriptblocks (param() wrappers sent by PSHelper). + # Module functions are trusted and can use any cmdlet internally regardless of this list. + VisibleCmdlets = @( + 'ConvertTo-Json', + 'ConvertFrom-Json', + 'Write-Output', + 'Write-Verbose', + 'Write-Warning', + 'Write-Error', + 'Write-Information' + ) + + # certutil.exe is called directly via the PowerShell pipeline by Get-CryptoProviders. + # Full path is required. Add-KeyfactorCertificate uses certutil via .NET ProcessStartInfo + # and does NOT need it listed here. + VisibleExternalCommands = @( + 'C:\Windows\System32\certutil.exe' + ) + + # No variables need to be pre-defined for the orchestrator. + VisibleVariables = @() + + # No aliases beyond what the module exports. + VisibleAliases = @() +} diff --git a/IISU/PowerShellScripts/WinADFSScripts.ps1 b/IISU/PowerShell/WinADFSScripts.ps1 similarity index 100% rename from IISU/PowerShellScripts/WinADFSScripts.ps1 rename to IISU/PowerShell/WinADFSScripts.ps1 diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShell/WinCertScripts.ps1 similarity index 99% rename from IISU/PowerShellScripts/WinCertScripts.ps1 rename to IISU/PowerShell/WinCertScripts.ps1 index 2d3830b3..9b1ec769 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShell/WinCertScripts.ps1 @@ -58,6 +58,7 @@ $InformationPreference = "Continue" # 300 Error Unknown or unhandled exception # 400 Error Invalid Ssl Flag bit combination +# Migrated function New-ResultObject { param( [ValidateSet("Success", "Warning", "Error", "Skipped")] @@ -79,6 +80,7 @@ function New-ResultObject { } } +# Migrated function Get-KFCertificates { param ( [Parameter(Mandatory = $false)] @@ -151,6 +153,7 @@ function Get-KFCertificates { } } +# Migrated function Add-KFCertificateToStore{ param ( [Parameter(Mandatory = $true)] @@ -280,6 +283,7 @@ function Add-KFCertificateToStore{ } } +# Migrated function Remove-KFCertificateFromStore { param ( [string]$Thumbprint, @@ -1736,6 +1740,7 @@ function Import-SignedCertificate { # Shared Functions # Function to get SAN (Subject Alternative Names) from a certificate +# Migrated function Get-KFSAN($cert) { $san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" } if ($san) { @@ -1745,6 +1750,7 @@ function Get-KFSAN($cert) { } #Function to verify if the given CSP is found on the computer +# Migrated function Test-CryptoServiceProvider { param( [Parameter(Mandatory = $true)] @@ -1761,6 +1767,7 @@ function Test-CryptoServiceProvider { } # Function that takes an x509 certificate object and returns the csp +# Migrated function Get-CertificateCSP { param( [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert @@ -1850,6 +1857,7 @@ function Get-CertificateCSP { } } +# Migrated function Get-CryptoProviders { # Retrieves the list of available Crypto Service Providers using certutil try { @@ -1878,6 +1886,7 @@ function Get-CryptoProviders { } } +# Migrated function Validate-CryptoProvider { param ( [Parameter(Mandatory)] diff --git a/IISU/RemoteSettings.cs b/IISU/RemoteSettings.cs index 43465b83..12b0d3c7 100644 --- a/IISU/RemoteSettings.cs +++ b/IISU/RemoteSettings.cs @@ -26,6 +26,8 @@ public class RemoteSettings public string ServerUserName { get; set; } public string ServerPassword { get; set; } + public bool UseJEA { get; set; } + public string JEAEndpointName { get; set; } = "keyfactor.wincert"; } } diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index 3e830324..34831156 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -50,11 +50,26 @@ PreserveNewest - + Always - + Always + + Always + + + Always + + + + + + + + + + From c45ef24f1f3bd4583c680f876674ae4341cf4cdb Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Fri, 24 Apr 2026 18:24:09 +0000 Subject: [PATCH 02/53] Update generated docs --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index adb31708..2ea905e7 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,9 @@ the Keyfactor Command Portal
Click to expand details -The IIS Bound Certificate Store Type, identified by its short name 'IISU,' is designed for the management of certificates bound to IIS (Internet Information Services) servers. This store type allows users to automate and streamline the process of adding, removing, and reenrolling certificates for IIS sites, making it significantly easier to manage web server certificates. +#### Key Features and Representation + +The IISU store type represents the IIS servers and their certificate bindings. It specifically caters to managing SSL/TLS certificates tied to IIS websites, allowing bind operations such as specifying site names, IP addresses, ports, and enabling Server Name Indication (SNI). By default, it supports job types like Inventory, Add, Remove, and Reenrollment, thereby offering comprehensive management capabilities for IIS certificates. #### Understanding SSL Flags @@ -462,6 +464,16 @@ For authoritative guidance on SSL bindings and the `sslFlags` property, refer to **SSL Flag 2 (Centralized Certificate Store)** is currently **not supported** by this implementation. Using this flag will result in an error and the job will not complete successfully. +#### Limitations and Areas of Confusion + +- **Caveats:** It's important to ensure that the Windows Remote Management (WinRM) is properly configured on the target server. The orchestrator relies on WinRM to perform its tasks, such as manipulating the Windows Certificate Stores. Misconfiguration of WinRM may lead to connection and permission issues. +

When performing Inventory, all bound certificates regardless to their store location will be returned. +

When executing an Add or Renew Management job, the Store Location will be considered and place the certificate in that location. + +- **Limitations:** Users should be aware that for this store type to function correctly, certain permissions are necessary. While some advanced users successfully use non-administrator accounts with specific permissions, it is officially supported only with Local Administrator permissions. Complexities with interactions between Group Policy, WinRM, User Account Control, and other environmental factors may impede operations if not properly configured. + +- **Custom Alias and Private Keys:** The store type does not support custom aliases for individual entries and requires private keys because IIS certificates without private keys would be invalid. + From 585fd7f97085e4b0784e6614ba21596af8c27d5a Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 24 Apr 2026 17:17:33 -0500 Subject: [PATCH 03/53] Helper updates --- IISU/PSHelper.cs | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index b4c1a77c..3ea733a9 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -152,8 +152,15 @@ public void Initialize() InitializeLocalSession(); } - // Display Hosting information - string psInfo = @" + // Display hosting information. + // Skipped in JEA sessions: [System.Environment] and [System.Net.Dns] are blocked + // by ConstrainedLanguage and this script runs as untrusted inline code, not as a + // trusted module function. + // TODO: Create Get-KeyfactorHostInfo in Keyfactor.WinCert.Common so JEA sessions + // can also log host details at startup. + if (!useJea) + { + string psInfo = @" $psVersion = $PSVersionTable.PSVersion $os = [System.Environment]::OSVersion $hostName = [System.Net.Dns]::GetHostName() @@ -164,10 +171,11 @@ public void Initialize() HostName = $hostName } | ConvertTo-Json "; - var results = ExecutePowerShell(psInfo, isScript: true); - foreach (var result in results) - { - _logger.LogTrace($"{result}"); + var results = ExecutePowerShell(psInfo, isScript: true); + foreach (var result in results) + { + _logger.LogTrace($"{result}"); + } } } @@ -766,14 +774,12 @@ public Collection ExecutePowerShellScript(string script) } else { - // For remote execution, use Invoke-Command - var scriptBlock = isScript - ? ScriptBlock.Create(commandOrScript) // Use the script as a ScriptBlock - : ScriptBlock.Create($"& {{ {commandOrScript} }}"); // Wrap commands in ScriptBlock - + // For remote execution use Invoke-Command. The command/script becomes the + // scriptblock body directly — no & { } child-scope wrapper, which can + // prevent JEA ConstrainedLanguage sessions from seeing visible functions. PS.AddCommand("Invoke-Command") .AddParameter("Session", _PSSession) - .AddParameter("ScriptBlock", scriptBlock); + .AddParameter("ScriptBlock", ScriptBlock.Create(commandOrScript)); } // Add Parameters if provided @@ -788,12 +794,11 @@ public Collection ExecutePowerShellScript(string script) } else { - // Remote execution: Use ArgumentList for parameters - var paramBlock = string.Join(", ", parameters.Select(p => - { - string typeName = p.Value?.GetType().Name ?? "object"; - return $"[{typeName}] ${p.Key}"; - })); + // Remote execution: Use ArgumentList for parameters. + // No type annotations in the param block — they are unnecessary for + // correct ArgumentList binding and some CLR types (arrays, nulls) produce + // names that break ConstrainedLanguage JEA sessions. + var paramBlock = string.Join(", ", parameters.Keys.Select(k => $"${k}")); var paramUsage = string.Join(" ", parameters.Select(p => $"-{p.Key} ${p.Key}")); From 3d8180af845142d02625712d3c7405161de62a7f Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 27 Apr 2026 13:27:43 -0700 Subject: [PATCH 04/53] Updated spelling of PowerShell script and fixed Information Messages from PW --- IISU/ImplementedStoreTypes/Win/Inventory.cs | 2 +- IISU/PSHelper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/IISU/ImplementedStoreTypes/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs index d5a8f934..d12c8964 100644 --- a/IISU/ImplementedStoreTypes/Win/Inventory.cs +++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs @@ -137,7 +137,7 @@ public List QueryWinCertCertificates(RemoteSettings settin { "StoreName", StoreName } }; - results = ps.ExecutePowerShell("Get-KefactorCertificates", parameters); + results = ps.ExecutePowerShell("Get-KeyfactorCertificates", parameters); // If there are certificates, deserialize the results and send them back to command if (results != null && results.Count > 0) diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 3ea733a9..5fc8e964 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -895,7 +895,7 @@ public static void ProcessPowerShellScriptEvent(object? sender, DataAddedEventAr if (infoMessages != null) { var infoMessage = infoMessages[e.Index]; - _logger.LogInformation($"INFO: {infoMessage.MessageData.ToString()}"); + _logger.LogInformation($"INFO: {infoMessage.MessageData}"); } break; From 59d239f25f436134480ab2906a742577e75b3c1f Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 27 Apr 2026 20:55:14 -0500 Subject: [PATCH 05/53] Added JEA support for IIS --- .../ImplementedStoreTypes/WinIIS/Inventory.cs | 2 +- .../WinIIS/Management.cs | 2 +- .../WinIIS/WinIISBinding.cs | 38 +++--- IISU/PSHelper.cs | 48 +++++-- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 29 +++-- .../Keyfactor.WinCert.IIS.psm1 | 37 ++++++ .../Private/Add-IISBindingWithSSL.ps1 | 104 +++++++++++++++ .../Private/Get-IISManagementInfo.ps1 | 54 ++++++++ .../Private/Get-ValidSslFlagsForSystem.ps1 | 32 +++++ .../Private/Test-IISDrive.ps1 | 23 ++++ .../Private/Test-ValidSslFlags.ps1 | 82 ++++++++++++ .../Get-KeyfactorIISBoundCertificates.ps1 | 76 +++++++++++ .../Public/New-KeyfactorIISSiteBinding.ps1 | 118 ++++++++++++++++++ .../Public/Remove-KeyfactorIISSiteBinding.ps1 | 69 ++++++++++ .../Keyfactor.WinCert.IIS.psrc | 49 ++++++++ IISU/PowerShell/WinCertScripts.ps1 | 15 +++ IISU/WindowsCertStore.csproj | 3 + 17 files changed, 734 insertions(+), 47 deletions(-) create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-IISDrive.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc diff --git a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs index 390c678a..66a679ae 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs @@ -144,7 +144,7 @@ public List QueryIISCertificates(RemoteSettings settings) // results = ps.InvokeFunction("Get-KFIISBoundCertificates"); //} - results = ps.ExecutePowerShell("Get-KFIISBoundCertificates"); + results = ps.ExecutePowerShell("Get-KeyfactorIISBoundCertificates"); // If there are certificates, deserialize the results and send them back to command if (results != null && results.Count > 0) diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index 2247431b..51386668 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -297,7 +297,7 @@ public string AddCertificate(string certificateContents, string privateKeyPasswo if (!string.IsNullOrEmpty(privateKeyPassword)) { parameters.Add("PrivateKeyPassword", privateKeyPassword); } if (!string.IsNullOrEmpty(cryptoProvider)) { parameters.Add("CryptoServiceProvider", cryptoProvider); } - _results = _psHelper.ExecutePowerShell("Add-KFCertificateToStore", parameters); + _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); _logger.LogTrace("Returned from executing PS function (Add-KFCertificateToStore)"); // This should return the thumbprint of the certificate diff --git a/IISU/ImplementedStoreTypes/WinIIS/WinIISBinding.cs b/IISU/ImplementedStoreTypes/WinIIS/WinIISBinding.cs index 678a2b4d..3f2ff418 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/WinIISBinding.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/WinIISBinding.cs @@ -34,7 +34,7 @@ public class WinIISBinding public static Collection BindCertificate(PSHelper psHelper, IISBindingInfo bindingInfo, string thumbprint, string renewalThumbprint, string storePath) { _logger = LogHandler.GetClassLogger(typeof(WinIISBinding)); - _logger.LogTrace("Attempting to bind and execute PS function (New-KFIISSiteBinding)"); + _logger.LogTrace("Attempting to bind and execute PS function (New-KeyfactorIISSiteBinding)"); // Mandatory parameters var parameters = new Dictionary @@ -53,7 +53,7 @@ public static Collection BindCertificate(PSHelper psHelper, IISBinding try { - return psHelper.ExecutePowerShell("New-KFIISSiteBinding", parameters); // returns true if successful + return psHelper.ExecutePowerShell("New-KeyfactorIISSiteBinding", parameters); } catch (Exception ex) { @@ -64,26 +64,20 @@ public static Collection BindCertificate(PSHelper psHelper, IISBinding public static bool UnBindCertificate(PSHelper psHelper, IISBindingInfo bindingInfo) { _logger = LogHandler.GetClassLogger(typeof(WinIISBinding)); - _logger.LogTrace("Attempting to UnBind and execute PS function (Remove-KFIISSiteBinding)"); + _logger.LogTrace("Attempting to UnBind and execute PS function (Remove-KeyfactorIISSiteBinding)"); + + string bindingInfoStr = $"{bindingInfo.IPAddress}:{bindingInfo.Port}:{bindingInfo.HostName ?? string.Empty}"; - // Mandatory parameters var parameters = new Dictionary { { "SiteName", bindingInfo.SiteName }, - { "IPAddress", bindingInfo.IPAddress }, - { "Port", bindingInfo.Port }, + { "BindingInfo", bindingInfoStr }, }; - // Optional parameters - if (!string.IsNullOrEmpty(bindingInfo.HostName)) - { - parameters.Add("HostName", bindingInfo.HostName); - } - try { - var results = psHelper.ExecutePowerShell("Remove-KFIISSiteBinding", parameters); - _logger.LogTrace("Returned from executing PS function (Remove-KFIISSiteBinding)"); + var results = psHelper.ExecutePowerShell("Remove-KeyfactorIISSiteBinding", parameters); + _logger.LogTrace("Returned from executing PS function (Remove-KeyfactorIISSiteBinding)"); if (results == null || results.Count == 0) { @@ -91,15 +85,13 @@ public static bool UnBindCertificate(PSHelper psHelper, IISBindingInfo bindingIn return false; } - if (results[0].BaseObject is bool success) - { - return success; - } - else - { - _logger.LogWarning("Unexpected result type from PowerShell function."); - return false; - } + string status = results[0].Properties["Status"]?.Value as string ?? string.Empty; + if (status == "Success" || status == "Skipped") + return true; + + string errorMsg = results[0].Properties["ErrorMessage"]?.Value as string ?? string.Empty; + _logger.LogWarning($"Remove-KeyfactorIISSiteBinding returned status '{status}': {errorMsg}"); + return false; } catch (Exception ex) { diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 5fc8e964..3fbdac3c 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -281,6 +281,15 @@ private void InitializeRemoteSession() { _logger.LogDebug($"JEA session active on endpoint '{jeaEndpoint}' - skipping script injection, functions are pre-registered."); } + + // Set $InformationPreference globally so Write-Information output is forwarded + // back to PSHelper's Information stream listener for all function calls in this session. + PS.AddCommand("Invoke-Command") + .AddParameter("Session", _PSSession) + .AddParameter("ScriptBlock", ScriptBlock.Create("$global:InformationPreference = 'Continue'")); + PS.Invoke(); + PS.Commands.Clear(); + _logger.LogTrace("Remote session preference variables configured."); } else { @@ -301,6 +310,13 @@ private void InitializeLocalSession() _logger.LogTrace("Setting Execution Policy to Unrestricted"); SetExecutionPolicyUnrestricted(); + // Set $InformationPreference globally so Write-Information output is forwarded + // back to PSHelper's Information stream listener for all function calls in this session. + PS.AddScript("$global:InformationPreference = 'Continue'"); + PS.Invoke(); + PS.Commands.Clear(); + _logger.LogTrace("Local session preference variables configured."); + // Check if ADFS module is available (only needed for ADFS stores) bool adfsModuleImported = false; if (this.isADFSStore) @@ -308,34 +324,42 @@ private void InitializeLocalSession() adfsModuleImported = ImportAdfsModule(); } - // Import the Keyfactor.WinCert.Common module (handles Private-before-Public load order internally) - _logger.LogTrace("Loading PowerShell scripts"); - string moduleFile = Path.Combine(scriptFileLocation, "Keyfactor.WinCert.Common", "Keyfactor.WinCert.Common.psm1"); - if (File.Exists(moduleFile)) + // Import all module .psm1 files alphabetically (ensures Common loads before IIS, etc.) + _logger.LogTrace("Loading PowerShell modules"); + var moduleDirs = Directory.GetDirectories(scriptFileLocation) + .OrderBy(d => d) + .ToList(); + + foreach (var moduleDir in moduleDirs) { - _logger.LogTrace($"Importing module: {moduleFile}"); + var moduleName = Path.GetFileName(moduleDir); + var modulePsm1 = Path.Combine(moduleDir, $"{moduleName}.psm1"); + + if (!File.Exists(modulePsm1)) + { + _logger.LogTrace($"No .psm1 found in {moduleName}, skipping"); + continue; + } + + _logger.LogTrace($"Importing module: {modulePsm1}"); PS.AddCommand("Import-Module") - .AddParameter("Name", moduleFile) + .AddParameter("Name", modulePsm1) .AddParameter("Force"); PS.Invoke(); if (PS.HadErrors) { - _logger.LogError("Errors importing Keyfactor.WinCert.Common module:"); + _logger.LogError($"Errors importing {moduleName} module:"); foreach (var error in PS.Streams.Error) _logger.LogError($" {error}"); PS.Streams.Error.Clear(); } else { - _logger.LogInformation("Keyfactor.WinCert.Common module imported successfully."); + _logger.LogInformation($"{moduleName} module imported successfully."); } PS.Commands.Clear(); } - else - { - _logger.LogWarning($"Keyfactor.WinCert.Common module not found at: {moduleFile}"); - } // Load flat legacy .ps1 scripts from the PowerShell root directory var rootScripts = Directory.GetFiles(scriptFileLocation, "*.ps1") diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc index 8e225248..80d1cbd5 100644 --- a/IISU/PowerShell/Build/KeyfactorWinCert.pssc +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -6,10 +6,13 @@ # PREREQUISITES (run once per target machine as Administrator) # ============================================================ # -# 1. Install the module to the system module path so it is treated as trusted code: +# 1. Install the modules to the system module path so they are treated as trusted code: # -# $dest = 'C:\Program Files\WindowsPowerShell\Modules\Keyfactor.WinCert.Common' -# Copy-Item -Path '.\Keyfactor.WinCert.Common' -Destination $dest -Recurse -Force +# $base = 'C:\Program Files\WindowsPowerShell\Modules' +# Copy-Item -Path '.\Keyfactor.WinCert.Common' -Destination "$base\Keyfactor.WinCert.Common" -Recurse -Force +# Copy-Item -Path '.\Keyfactor.WinCert.IIS' -Destination "$base\Keyfactor.WinCert.IIS" -Recurse -Force +# +# (Only install the modules needed for the store types you use on this endpoint.) # # 2. Create the transcript directory for audit logging: # @@ -36,15 +39,18 @@ # TESTING THE ENDPOINT # ============================================================ # -# # Connect and list available commands (should show only the 4 Keyfactor functions) +# # Connect and list available commands # $s = New-PSSession -ComputerName ` # -ConfigurationName 'keyfactor.wincert' ` # -Credential (Get-Credential) # Invoke-Command -Session $s -ScriptBlock { Get-Command } # -# # Run an inventory +# # Test WinCert inventory # Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } # +# # Test IIS inventory (requires Keyfactor.WinCert.IIS module installed) +# Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorIISBoundCertificates } +# # Remove-PSSession $s # # ============================================================ @@ -77,14 +83,17 @@ TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' # --- Role Definitions --- - # Map the connecting user/group to the Keyfactor.WinCert.Common role capability. + # Map the connecting user/group to one or more Keyfactor role capabilities. # Replace 'BUILTIN\Administrators' with the AD group or local group whose members # are authorized to connect (e.g. 'DOMAIN\KeyfactorOrchestrators'). # - # Multiple groups can be granted the same or different roles: - # 'DOMAIN\CertAdmins' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } - # 'DOMAIN\CertReadOnly' = @{ RoleCapabilities = 'Keyfactor.WinCert.ReadOnly' } + # Add only the role capabilities whose modules are installed on this machine. + # Multiple capabilities are merged — the session exposes all their VisibleFunctions. + # + # Examples: + # WinCert + IIS: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } + # WinCert only: @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } RoleDefinitions = @{ - 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } + 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } } } diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 new file mode 100644 index 00000000..8e54a0ab --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 @@ -0,0 +1,37 @@ +# Keyfactor.WinCert.IIS.psm1 +# +# Static explicit dot-sourcing is used instead of Get-ChildItem/Select-Object discovery. +# This avoids a dependency on proxy-restricted cmdlets during JEA session initialization. +# (In RestrictedRemoteServer, Select-Object -ExpandProperty is not available in the proxy.) + +# Ensure Keyfactor.WinCert.Common is available (provides New-KeyfactorResult, +# Get-CertificateCSP, Get-CertificateSAN, etc.). +# In JEA sessions the .psrc lists both modules for import. +# In local non-JEA sessions PSHelper imports modules alphabetically, so Common loads first. +# This block is a fallback for standalone / development use. +if (-not (Get-Command 'New-KeyfactorResult' -ErrorAction SilentlyContinue)) { + $commonModulePath = Join-Path $PSScriptRoot '..\Keyfactor.WinCert.Common\Keyfactor.WinCert.Common.psm1' + if (Test-Path $commonModulePath) { + Import-Module $commonModulePath -Force + } +} + +# Private helpers — load in dependency order (no-dependency functions first) +. "$PSScriptRoot\Private\Get-ValidSslFlagsForSystem.ps1" +. "$PSScriptRoot\Private\Test-IISDrive.ps1" +. "$PSScriptRoot\Private\Test-ValidSslFlags.ps1" +. "$PSScriptRoot\Private\Add-IISBindingWithSSL.ps1" +. "$PSScriptRoot\Private\Get-IISManagementInfo.ps1" + +# Public functions +. "$PSScriptRoot\Public\Get-KeyfactorIISBoundCertificates.ps1" +. "$PSScriptRoot\Public\Remove-KeyfactorIISSiteBinding.ps1" +. "$PSScriptRoot\Public\New-KeyfactorIISSiteBinding.ps1" + +# Export only public functions for non-JEA use. +# In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. +Export-ModuleMember -Function @( + 'Get-KeyfactorIISBoundCertificates', + 'New-KeyfactorIISSiteBinding', + 'Remove-KeyfactorIISSiteBinding' +) diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 new file mode 100644 index 00000000..8ba77bee --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 @@ -0,0 +1,104 @@ +function Add-IISBindingWithSSL { + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [string]$Protocol, + + [Parameter(Mandatory = $true)] + [string]$IPAddress, + + [Parameter(Mandatory = $true)] + [int]$Port, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Hostname, + + [string]$Thumbprint, + + [string]$StoreName = "My", + + [int]$SslFlags = 0, + + [Parameter(Mandatory = $true)] + [bool]$UseIISDrive + ) + + Write-Verbose "Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" + + try { + if ($UseIISDrive) { + # Add binding using WebAdministration module + $bindingParams = @{ + Name = $SiteName + Protocol = $Protocol + IPAddress = $IPAddress + Port = $Port + SslFlags = $SslFlags + } + + # Only add HostHeader if it's not empty (New-WebBinding doesn't like empty strings) + if (-not [string]::IsNullOrEmpty($Hostname)) { + $bindingParams.HostHeader = $Hostname + } + + Write-Verbose "Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" + New-WebBinding @bindingParams + + # Bind SSL certificate if HTTPS + if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + Write-Verbose "Searching for binding: $searchBindings" + + $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { + $_.bindingInformation -eq $searchBindings + } + + if ($binding) { + Write-Verbose "Binding SSL certificate with thumbprint: $Thumbprint" + $null = $binding.AddSslCertificate($Thumbprint, $StoreName) + Write-Verbose "SSL certificate successfully bound" + return New-KeyfactorResult -Status Success -Code 0 -Step BindSSL -Message "Binding and SSL certificate successfully applied" + } else { + return New-KeyfactorResult -Status Error -Code 202 -Step BindSSL -ErrorMessage "No binding found for: $searchBindings" + } + } + else { + return New-KeyfactorResult -Status Success -Code 0 -Step AddBinding -Message "HTTP binding successfully added" + } + } + else { + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] + + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + $newBinding = $site.Bindings.Add($searchBindings, $Protocol) + + if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { + # Clean and convert thumbprint to byte array + $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' + $hashBytes = for ($i = 0; $i -lt $cleanThumbprint.Length; $i += 2) { + [Convert]::ToByte($cleanThumbprint.Substring($i, 2), 16) + } + + $newBinding.CertificateStoreName = $StoreName + $newBinding.CertificateHash = [byte[]]$hashBytes + $newBinding.SetAttributeValue("sslFlags", $SslFlags) + } + + $iis.CommitChanges() + return New-KeyfactorResult -Status Success -Code 0 -Step BindSSL -Message "Binding and certificate successfully applied via ServerManager" + } + } + catch { + $errorMessage = "Error adding binding with SSL: $($_.Exception.Message)" + Write-Warning $errorMessage + return New-KeyfactorResult -Status Error -Code 202 -Step AddBinding -ErrorMessage $errorMessage + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 new file mode 100644 index 00000000..ded1400b --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 @@ -0,0 +1,54 @@ +function Get-IISManagementInfo { + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName + ) + + $hasIISDrive = Test-IISDrive + Write-Verbose "IIS Drive available: $hasIISDrive" + + if ($hasIISDrive) { + $null = Import-Module WebAdministration + $sitePath = "IIS:\Sites\$SiteName" + + if (-not (Test-Path $sitePath)) { + $errorMessage = "Site '$SiteName' not found in IIS drive" + Write-Error $errorMessage + return @{ + Success = $false + UseIISDrive = $true + Result = New-KeyfactorResult -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } + } + } + + return @{ + Success = $true + UseIISDrive = $true + Result = $null + } + } + else { + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] + + if ($null -eq $site) { + $errorMessage = "Site '$SiteName' not found in ServerManager" + Write-Error $errorMessage + return @{ + Success = $false + UseIISDrive = $false + Result = New-KeyfactorResult -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } + } + } + + return @{ + Success = $true + UseIISDrive = $false + Result = $null + } + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 new file mode 100644 index 00000000..f92bd6db --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 @@ -0,0 +1,32 @@ +function Get-ValidSslFlagsForSystem { + <# + .SYNOPSIS + Gets the valid SSL flag bits for the current Windows Server version + #> + [CmdletBinding()] + param() + + $build = [System.Environment]::OSVersion.Version.Build + + # Return array of valid flag values based on Windows Server version + if ($build -ge 20348) { + # Windows Server 2022+ (IIS 10.0.20348+) + Write-Verbose "Detected Windows Server 2022 or later (Build: $build)" + return @(1, 4, 8, 16, 32, 64) # Include unknowns for testing + } + elseif ($build -ge 17763) { + # Windows Server 2019 (IIS 10.0.17763) + Write-Verbose "Detected Windows Server 2019 (Build: $build)" + return @(1, 4, 8) + } + elseif ($build -ge 14393) { + # Windows Server 2016 (IIS 10.0) + Write-Verbose "Detected Windows Server 2016 (Build: $build)" + return @(1, 4) + } + else { + # Windows Server 2012 R2 and earlier (IIS 8.5) + Write-Verbose "Detected Windows Server 2012 R2 or earlier (Build: $build)" + return @(1, 2) + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-IISDrive.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-IISDrive.ps1 new file mode 100644 index 00000000..65b31613 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-IISDrive.ps1 @@ -0,0 +1,23 @@ +function Test-IISDrive { + [CmdletBinding()] + param () + + # Try to import the WebAdministration module if not already loaded + if (-not (Get-Module -Name WebAdministration)) { + try { + $null = Import-Module WebAdministration -ErrorAction Stop + } + catch { + Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." + return $false + } + } + + # Check if IIS drive is available + if (-not (Get-PSDrive -Name 'IIS' -ErrorAction SilentlyContinue)) { + Write-Warning "IIS:\ drive not available. Ensure IIS is installed and the WebAdministration module is imported." + return $false + } + + return $true +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 new file mode 100644 index 00000000..c0762980 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 @@ -0,0 +1,82 @@ +function Test-ValidSslFlags { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [int]$Flags, + + [Parameter(Mandatory = $false)] + [switch]$ThrowOnError + ) + + $build = [System.Environment]::OSVersion.Version.Build + $validBits = Get-ValidSslFlagsForSystem + + # Calculate valid bitmask + $validMask = 0 + foreach ($bit in $validBits) { + $validMask = $validMask -bor $bit + } + + # Check for unknown/unsupported bits + $unknownBits = $Flags -band (-bnot $validMask) + if ($unknownBits -ne 0) { + $errorMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) contains unsupported bits " + + "for this Windows Server version (Build: $build): $unknownBits (0x$($unknownBits.ToString('X'))). " + + "Supported flags: $($validBits -join ', ')" + + if ($ThrowOnError) { + throw $errorMsg + } + else { + return [PSCustomObject]@{ + IsValid = $false + ErrorCode = 400 + Message = $errorMsg + WindowsBuild = $build + ValidFlags = $validBits + InvalidBits = $unknownBits + } + } + } + + # Check for known invalid combinations + $hasSni = ($Flags -band 1) -ne 0 + $hasCentralCert = ($Flags -band 2) -ne 0 + + if ($hasCentralCert -and -not $hasSni) { + $errorMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is invalid: " + + "CentralCertStore (0x2) requires SNI (0x1) to be enabled." + + if ($ThrowOnError) { + throw $errorMsg + } + else { + return [PSCustomObject]@{ + IsValid = $false + ErrorCode = 400 + Message = $errorMsg + WindowsBuild = $build + ValidFlags = $validBits + InvalidBits = 0 + } + } + } + + # Validation passed + $successMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is valid for this system (Build: $build)." + + if ($ThrowOnError) { + Write-Verbose $successMsg + return $true + } + else { + return [PSCustomObject]@{ + IsValid = $true + ErrorCode = 0 + Message = $successMsg + WindowsBuild = $build + ValidFlags = $validBits + InvalidBits = 0 + } + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1 new file mode 100644 index 00000000..966af65f --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1 @@ -0,0 +1,76 @@ +function Get-KeyfactorIISBoundCertificates{ + $certificates = @() + $totalBoundCertificates = 0 + + try { + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" # -AssemblyName "Microsoft.Web.Administration" + $serverManager = New-Object Microsoft.Web.Administration.ServerManager + } catch { + Write-Error "Failed to create ServerManager. IIS might not be installed." + return + } + + $websites = $serverManager.Sites + Write-Information "There were $($websites.Count) websites found." + + foreach ($site in $websites) { + $siteName = $site.Name + $siteBoundCertificateCount = 0 + + foreach ($binding in $site.Bindings) { + if ($binding.Protocol -eq 'https' -and $binding.CertificateHash) { + $certHash = ($binding.CertificateHash | ForEach-Object { $_.ToString("X2") }) -join "" + $storeName = if ($binding.CertificateStoreName) { $binding.CertificateStoreName } else { "My" } + + try { + $cert = Get-ChildItem -Path "Cert:\LocalMachine\$storeName" | Where-Object { + $_.Thumbprint -eq $certHash + } + + if (-not $cert) { + Write-Warning "Certificate with thumbprint not found in Cert:\LocalMachine\$storeName" + continue + } + + $certBase64 = [Convert]::ToBase64String($cert.RawData) + $ip, $port, $hostname = $binding.BindingInformation -split ":", 3 + + $certInfo = [PSCustomObject]@{ + SiteName = $siteName + Binding = $binding.BindingInformation + IPAddress = $ip + Port = $port + Hostname = $hostname + Protocol = $binding.Protocol + SNI = $binding.SslFlags + ProviderName = Get-CertificateCSP $cert + SAN = Get-CertificateSAN $cert + Certificate = $cert.Subject + ExpiryDate = $cert.NotAfter + Issuer = $cert.Issuer + Thumbprint = $cert.Thumbprint + HasPrivateKey = $cert.HasPrivateKey + CertificateBase64 = $certBase64 + } + + $certificates += $certInfo + $siteBoundCertificateCount++ + $totalBoundCertificates++ + } catch { + Write-Warning "Could not retrieve certificate details for hash $certHash in store $storeName." + Write-Warning $_ + } + } + } + + Write-Information "Website: $siteName has $siteBoundCertificateCount bindings with certificates." + } + + Write-Information "A total of $totalBoundCertificates bindings with valid certificates were found." + + if ($totalBoundCertificates -gt 0) { + $certificates | ConvertTo-Json + } else { + Write-Information "No valid certificates were found bound to websites." + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 new file mode 100644 index 00000000..6e5cdcc3 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 @@ -0,0 +1,118 @@ +function New-KeyfactorIISSiteBinding { + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName, + [string]$IPAddress = "*", + [int]$Port = 443, + [AllowEmptyString()] + [string]$Hostname = "", + [ValidateSet("http", "https")] + [string]$Protocol = "https", + [ValidateScript({ + if ($Protocol -eq 'https' -and [string]::IsNullOrEmpty($_)) { + throw "Thumbprint is required when Protocol is 'https'" + } + $true + })] + [string]$Thumbprint, + [string]$StoreName = "My", + [int]$SslFlags = 0 + ) + + Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue + Write-Verbose "Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" + + try { + # Step 1: Perform verifications and get management info + # Check SslFlags + $sslValidationResult = Test-ValidSslFlags -Flags $SslFlags + if (-not $sslValidationResult.IsValid) { + return New-KeyfactorResult -Status Error -Code 400 -Step "SSL Validation" -ErrorMessage $sslValidationResult.Message + } + + $managementInfo = Get-IISManagementInfo -SiteName $SiteName + if (-not $managementInfo.Success) { + return $managementInfo.Result + } + + # Step 2: Remove existing HTTPS bindings for this binding info + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + Write-Verbose "Removing existing HTTPS bindings for: $searchBindings" + + $removalResult = Remove-KeyfactorIISSiteBinding -SiteName $SiteName -BindingInfo $searchBindings -UseIISDrive $managementInfo.UseIISDrive + if ($removalResult.Status -eq 'Error') { + return $removalResult + } + + # Step 3: Determine SslFlags supported by Microsoft.Web.Administration + if ($SslFlags -gt 3) { + Write-Verbose "SslFlags value $SslFlags exceeds managed API range (0–3). Applying reduced flags for creation." + $SslFlagsApplied = ($SslFlags -band 3) + } else { + $SslFlagsApplied = $SslFlags + } + + # Step 4: Add the new binding with the reduced flag set + Write-Verbose "Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" + + $addParams = @{ + SiteName = $SiteName + Protocol = $Protocol + IPAddress = $IPAddress + Port = $Port + Hostname = $Hostname + Thumbprint = $Thumbprint + StoreName = $StoreName + SslFlags = $SslFlagsApplied + UseIISDrive = $managementInfo.UseIISDrive + } + + $addResult = Add-IISBindingWithSSL @addParams + + if ($addResult.Status -eq 'Error') { + return $addResult + } + + # Step 5: If extended flags, update via appcmd.exe + if ($SslFlags -gt 3) { + Write-Verbose "Applying full SslFlags=$SslFlags via appcmd" + + $appcmd = Join-Path $env:windir "System32\inetsrv\appcmd.exe" + + # Escape any single quotes in hostname + $safeHostname = $Hostname -replace "'", "''" + $bindingInfo = "${IPAddress}:${Port}:${safeHostname}" + + # Quote site name only if it contains spaces + if ($SiteName -match '\s') { + $siteArg = "/site.name:`"$SiteName`"" + } else { + $siteArg = "/site.name:$SiteName" + } + + # Build binding argument for appcmd + $bindingArg = "/bindings.[protocol='https',bindingInformation='$bindingInfo'].sslFlags:$SslFlags" + + Write-Verbose "Running appcmd: $appcmd $siteArg $bindingArg" + $appcmdOutput = & $appcmd set site $siteArg $bindingArg 2>&1 + Write-Verbose "appcmd output: $appcmdOutput" + + #& $appcmd set site $siteArg $bindingArg | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Warning "appcmd failed to set extended SslFlags ($SslFlags) for binding $bindingInfo." + } else { + Write-Verbose "Successfully updated SslFlags to $SslFlags via appcmd." + } + } + + return $addResult + } + catch { + $errorMessage = "Unexpected error in New-KFIISSiteBinding: $($_.Exception.Message)" + Write-Error $errorMessage + return New-KeyfactorResult -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 new file mode 100644 index 00000000..baf0a150 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 @@ -0,0 +1,69 @@ +function Remove-KeyfactorIISSiteBinding { + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$BindingInfo, + + [Parameter(Mandatory = $false)] + [System.Nullable[bool]]$UseIISDrive = $null + ) + + # Auto-detect IIS Drive availability if not explicitly provided + if ($null -eq $UseIISDrive) { + $UseIISDrive = Test-IISDrive + } + + Write-Verbose "Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" + + try { + if ($UseIISDrive) { + Write-Verbose "Using IIS Drive to remove binding" + $sitePath = "IIS:\Sites\$SiteName" + $site = Get-Item $sitePath + $httpsBindings = $site.Bindings.Collection | Where-Object { + $_.bindingInformation -eq $BindingInfo -and $_.protocol -eq "https" + } + + foreach ($binding in $httpsBindings) { + $bindingInfo = $binding.GetAttributeValue("bindingInformation") + $protocol = $binding.protocol + + Write-Verbose "Removing binding: $bindingInfo ($protocol)" + Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false + Write-Verbose "Successfully removed binding" + } + } + else { + Write-Verbose "Using Web Administration assembly to remove binding" + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] + + $httpsBindings = $site.Bindings | Where-Object { + $_.BindingInformation -eq $BindingInfo -and $_.Protocol -eq "https" + } + + foreach ($binding in $httpsBindings) { + Write-Verbose "Removing binding: $($binding.BindingInformation)" + $site.Bindings.Remove($binding) + Write-Verbose "Successfully removed binding" + } + + $iis.CommitChanges() + Write-Verbose "Committed changes to IIS" + } + + return New-KeyfactorResult -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" + } + catch { + $errorMessage = "Error removing existing binding: $($_.Exception.Message)" + Write-Warning $errorMessage + return New-KeyfactorResult -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $errorMessage + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc b/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc new file mode 100644 index 00000000..fce84114 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc @@ -0,0 +1,49 @@ +# +# Keyfactor.WinCert.IIS.psrc +# JEA Role Capability file for Keyfactor IIS certificate management operations +# +# Place this file under the module's RoleCapabilities directory so JEA can discover it: +# C:\Program Files\WindowsPowerShell\Modules\Keyfactor.WinCert.IIS\RoleCapabilities\Keyfactor.WinCert.IIS.psrc +# +# Reference this capability in KeyfactorWinCert.pssc: +# RoleDefinitions = @{ +# 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } +# } +# +@{ + GUID = '3527aec5-b87f-4bb4-98df-34f5b2ac49cb' + Author = 'Keyfactor' + Description = 'Role capability for Keyfactor IIS certificate management operations' + CompanyName = 'Keyfactor' + Copyright = '2025 Keyfactor' + + # Both modules must be installed under C:\Program Files\WindowsPowerShell\Modules\ + # so they are treated as fully trusted code regardless of the session LanguageMode. + ModulesToImport = @('Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS') + + # Only the three public IIS functions need to be visible to the caller. + # Internal helpers (New-KeyfactorResult, Get-CertificateCSP, Add-IISBindingWithSSL, etc.) + # are called from within trusted module code and do not require explicit exposure. + VisibleFunctions = @( + 'Get-KeyfactorIISBoundCertificates', + 'New-KeyfactorIISSiteBinding', + 'Remove-KeyfactorIISSiteBinding' + ) + + VisibleCmdlets = @( + 'ConvertTo-Json', + 'ConvertFrom-Json', + 'Write-Output', + 'Write-Verbose', + 'Write-Warning', + 'Write-Error', + 'Write-Information' + ) + + # appcmd.exe is invoked inside New-KeyfactorIISSiteBinding for extended SSL flag support. + # Trusted module code can call external commands unrestricted, but listing it here + # makes the allowed command surface explicit. + VisibleExternalCommands = @( + 'C:\Windows\System32\inetsrv\appcmd.exe' + ) +} diff --git a/IISU/PowerShell/WinCertScripts.ps1 b/IISU/PowerShell/WinCertScripts.ps1 index 9b1ec769..f03a3c1d 100644 --- a/IISU/PowerShell/WinCertScripts.ps1 +++ b/IISU/PowerShell/WinCertScripts.ps1 @@ -338,6 +338,7 @@ function Remove-KFCertificateFromStore { } ##### IIS Functions +#Migrated function Get-KFIISBoundCertificates { $certificates = @() $totalBoundCertificates = 0 @@ -414,6 +415,8 @@ function Get-KFIISBoundCertificates { Write-Information "No valid certificates were found bound to websites." } } + +# Migrated function New-KFIISSiteBinding { [CmdletBinding()] [OutputType([pscustomobject])] @@ -532,6 +535,8 @@ function New-KFIISSiteBinding { return New-ResultObject -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage } } + +# Migrated function Remove-ExistingIISBinding { [CmdletBinding()] [OutputType([pscustomobject])] @@ -596,6 +601,8 @@ function Remove-ExistingIISBinding { return New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $errorMessage } } + +# Migrated function Add-IISBindingWithSSL { [CmdletBinding()] [OutputType([pscustomobject])] @@ -2009,6 +2016,7 @@ function Parse-DNSubject { } #### Functions to test SSL flags +# Migrated function Get-ValidSslFlagsForSystem { <# .SYNOPSIS @@ -2041,6 +2049,8 @@ function Get-ValidSslFlagsForSystem { return @(1, 2) } } + +# Migrated function Test-ValidSslFlags { [CmdletBinding()] param( @@ -2123,6 +2133,8 @@ function Test-ValidSslFlags { } } } + +# Not used function Get-SslFlagDescription { <# .SYNOPSIS @@ -2156,6 +2168,7 @@ function Get-SslFlagDescription { # Note: Removed Test-IISBindingConflict function - we now mimic IIS behavior # IIS replaces exact matches and allows multiple hostnames (SNI) on same IP:Port +# Migrated function Get-IISManagementInfo { [CmdletBinding()] [OutputType([hashtable])] @@ -2210,6 +2223,8 @@ function Get-IISManagementInfo { } } } + +# Migrated function Ensure-IISDrive { [CmdletBinding()] param () diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index 34831156..b98e73a8 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -53,6 +53,9 @@ Always + + Always + Always From d3a92d4fc38408b799a659f14e1f9b3ffa1a557d Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 27 Apr 2026 23:33:47 -0500 Subject: [PATCH 06/53] Fixed and updated IIS Components --- .../Keyfactor.WinCert.Common.psm1 | 6 +++++- .../RoleCapabilities/Keyfactor.WinCert.Common.psrc | 8 +++++--- .../Public/New-KeyfactorIISSiteBinding.ps1 | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 index 9cfdaa96..45fec70d 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 @@ -20,8 +20,12 @@ # Export only public functions for non-JEA use. # In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. Export-ModuleMember -Function @( + # Public functions 'New-KeyfactorResult', 'Get-KeyfactorCertificates', 'Add-KeyfactorCertificate', - 'Remove-KeyfactorCertificate' + 'Remove-KeyfactorCertificate', + # Shared certificate inspection utilities — exported so other modules (e.g. IIS) can call them + 'Get-CertificateCSP', + 'Get-CertificateSAN' ) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc index 99d8e2a7..875fd41c 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc @@ -29,13 +29,15 @@ ModulesToImport = @('Keyfactor.WinCert.Common') # Functions the orchestrator is allowed to invoke in this JEA session. - # Private helper functions (Get-CertificateCSP, Get-CertificateSAN, etc.) are intentionally - # excluded -- they are called internally by the public functions, not by the orchestrator. + # Get-CertificateCSP and Get-CertificateSAN are also listed because other modules + # (e.g. Keyfactor.WinCert.IIS) call them cross-module, which requires them to be exported. VisibleFunctions = @( 'Get-KeyfactorCertificates', 'Add-KeyfactorCertificate', 'Remove-KeyfactorCertificate', - 'New-KeyfactorResult' + 'New-KeyfactorResult', + 'Get-CertificateCSP', + 'Get-CertificateSAN' ) # Cmdlets available to caller scriptblocks (param() wrappers sent by PSHelper). diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 index 6e5cdcc3..2de40982 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 @@ -48,7 +48,7 @@ function New-KeyfactorIISSiteBinding { # Step 3: Determine SslFlags supported by Microsoft.Web.Administration if ($SslFlags -gt 3) { - Write-Verbose "SslFlags value $SslFlags exceeds managed API range (0–3). Applying reduced flags for creation." + Write-Verbose "SslFlags value $SslFlags exceeds managed API range (0-3). Applying reduced flags for creation." $SslFlagsApplied = ($SslFlags -band 3) } else { $SslFlagsApplied = $SslFlags From 355c3acfd50865b9135d4f2ffacde0d679e6fc3d Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 29 Apr 2026 21:03:54 -0500 Subject: [PATCH 07/53] Completed SQL Module and updated test projects --- IISU/ImplementedStoreTypes/Win/Inventory.cs | 3 +- IISU/ImplementedStoreTypes/Win/Management.cs | 5 +- .../ImplementedStoreTypes/WinIIS/Inventory.cs | 3 +- .../WinIIS/Management.cs | 5 +- .../ImplementedStoreTypes/WinSQL/Inventory.cs | 14 +- .../WinSQL/Management.cs | 13 +- .../WinSQL/WinSqlBinding.cs | 16 +- IISU/Models/JobProperties.cs | 8 +- IISU/PSHelper.cs | 39 +- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 9 +- .../Keyfactor.WinCert.Common.psm1 | 3 + .../Private/Convert-DNSSubject.ps1 | 103 ++++ .../Private/Get-CertificateCSP.ps1 | 16 +- .../Private/Get-CryptoProviders.ps1 | 8 +- .../Private/Validate-CryptoProvider.ps1 | 6 +- .../Public/Add-KeyfactorCertificate.ps1 | 10 +- .../Public/New-KeyfactorODKGEnrollment.ps1 | 97 ++++ .../Keyfactor.WinCert.Common.psrc | 7 +- .../Private/Add-IISBindingWithSSL.ps1 | 12 +- .../Private/Get-IISManagementInfo.ps1 | 4 +- .../Private/Get-ValidSslFlagsForSystem.ps1 | 10 +- .../Private/Test-ValidSslFlags.ps1 | 4 +- .../Public/New-KeyfactorIISSiteBinding.ps1 | 18 +- .../Public/Remove-KeyfactorIISSiteBinding.ps1 | 18 +- .../Keyfactor.WinCert.SQL.psm1 | 29 + .../Private/Get-KeyfactorSQLServiceName.ps1 | 10 + .../Private/Get-KeyfactorSQLServiceUser.ps1 | 19 + .../Set-KeyfactorSQLCertificateBinding.ps1 | 547 ++++++++++++++++++ .../Public/Get-KeyfactorSQLInventory.ps1 | 70 +++ .../Public/New-KeyfactorSQLBinding.ps1 | 60 ++ .../Public/Remove-KeyfactorSQLCertificate.ps1 | 56 ++ .../Keyfactor.WinCert.SQL.psrc | 61 ++ IISU/PowerShell/WinADFSScripts.ps1 | 16 +- IISU/PowerShell/WinCertScripts.ps1 | 202 +++---- IISU/Properties/AssemblyInfo.cs | 4 + IISU/RemoteSettings.cs | 3 +- IISU/WindowsCertStore.csproj | 3 + .../ClientConnection.cs | 4 +- .../Factories/ConnectionFactory.cs | 82 ++- .../WinCertIntegrationTests.cs | 224 +++++++ .../WinIISIntegrationTests.cs | 8 +- .../WinSQLIntegrationTests.cs | 10 +- .../servers.json | 7 +- .../JobPropertiesTests.cs | 76 +++ .../PSHelperConfigTests.cs | 43 ++ .../PSHelperUnitTests.cs | 2 +- .../WindowsCertStore.UnitTests.csproj | 7 +- 47 files changed, 1721 insertions(+), 253 deletions(-) create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Private/Convert-DNSSubject.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorODKGEnrollment.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Keyfactor.WinCert.SQL.psm1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceName.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Get-KeyfactorSQLInventory.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Public/New-KeyfactorSQLBinding.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Remove-KeyfactorSQLCertificate.ps1 create mode 100644 IISU/PowerShell/Keyfactor.WinCert.SQL/RoleCapabilities/Keyfactor.WinCert.SQL.psrc create mode 100644 IISU/Properties/AssemblyInfo.cs create mode 100644 WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs create mode 100644 WindowsCertStore.UnitTests/JobPropertiesTests.cs create mode 100644 WindowsCertStore.UnitTests/PSHelperConfigTests.cs diff --git a/IISU/ImplementedStoreTypes/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs index d12c8964..cbb22b39 100644 --- a/IISU/ImplementedStoreTypes/Win/Inventory.cs +++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs @@ -81,7 +81,6 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; - settings.UseJEA = jobProperties.UseJEA; settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace($"Querying Window certificate in store: {storePath}"); @@ -128,7 +127,7 @@ public List QueryWinCertCertificates(RemoteSettings settin { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index 672f2b91..440b4f15 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -88,10 +88,9 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; - bool useJea = jobProperties?.UseJEA ?? false; - string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? ""; - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint); switch (_operationType) { diff --git a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs index 66a679ae..f5214850 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs @@ -83,7 +83,6 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; - settings.UseJEA = jobProperties.UseJEA; settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace("Querying IIS Inventory.."); @@ -129,7 +128,7 @@ public List QueryIISCertificates(RemoteSettings settings) { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index 51386668..b9067c08 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -92,14 +92,13 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; - bool useJea = jobProperties?.UseJEA ?? false; - string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? ""; string alias = config.JobCertificate?.Alias?.Split(':').FirstOrDefault() ?? string.Empty; // Thumbprint is first part of the alias // Assign the binding information IISBindingInfo bindingInfo = new IISBindingInfo(config.JobProperties); - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint); _psHelper.Initialize(); diff --git a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs index 4e1f3fff..bbf637e4 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs @@ -81,11 +81,10 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn settings.IncludePortInSPN = jobProperties.SpnPortFlag; settings.ServerUserName = serverUserName; settings.ServerPassword = serverPassword; - settings.UseJEA = jobProperties.UseJEA; settings.JEAEndpointName = jobProperties.JEAEndpointName; _logger.LogTrace($"Attempting to read bound SQL Server certificates from cert store: {storePath}"); - inventoryItems = QuerySQLCertificates(settings, storePath); + inventoryItems = QuerySQLCertificates(settings); _logger.LogTrace("Invoking submitInventory.."); submitInventoryUpdate.Invoke(inventoryItems); @@ -123,20 +122,15 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn } } - public List QuerySQLCertificates(RemoteSettings settings, string storeName) + public List QuerySQLCertificates(RemoteSettings settings) { List Inventory = new(); - using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, useJea: settings.UseJEA, jeaEndpoint: settings.JEAEndpointName)) + using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName)) { ps.Initialize(); - var parameters = new Dictionary - { - { "StoreName", storeName } - }; - - results = ps.ExecutePowerShell("GET-KFSQLInventory", parameters); + results = ps.ExecutePowerShell("Get-KeyfactorSQLInventory"); // If there are certificates, deserialize the results and send them back to command if (results != null && results.Count > 0) diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index 61be06d3..c7cf289c 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -91,8 +91,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; - bool useJea = jobProperties?.UseJEA ?? false; - string jeaEndpoint = jobProperties?.JEAEndpointName ?? "keyfactor.wincert"; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? ""; RestartSQLService = jobProperties.RestartService; @@ -106,7 +105,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) RenewalThumbprint = config.JobProperties["RenewalThumbprint"]?.ToString(); } - _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, useJea: useJea, jeaEndpoint: jeaEndpoint); + _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint); _psHelper.Initialize(); @@ -236,8 +235,8 @@ public JobResult RemoveCertificate(string thumbprint) { "StorePath", _storePath } }; - _psHelper.ExecutePowerShell("Remove-KFCertificateFromStore", parameters); - _logger.LogTrace("Returned from executing PS function (Remove-KFCertificateFromStore)"); + _psHelper.ExecutePowerShell("Remove-KeyfactorCertificate", parameters); + _logger.LogTrace("Returned from executing PS function (Remove-KeyfactorCertificate)"); _psHelper.Terminate(); } @@ -282,8 +281,8 @@ public string AddCertificate(string certificateContents, string privateKeyPasswo if (!string.IsNullOrEmpty(privateKeyPassword)) { parameters.Add("PrivateKeyPassword", privateKeyPassword); } if (!string.IsNullOrEmpty(cryptoProvider)) { parameters.Add("CryptoServiceProvider", cryptoProvider); } - _results = _psHelper.ExecutePowerShell("Add-KFCertificateToStore", parameters); - _logger.LogTrace("Returned from executing PS function (Add-KFCertificateToStore)"); + _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); + _logger.LogTrace("Returned from executing PS function (Add-KeyfactorCertificate)"); // This should return the thumbprint of the certificate if (_results != null && _results.Count > 0) diff --git a/IISU/ImplementedStoreTypes/WinSQL/WinSqlBinding.cs b/IISU/ImplementedStoreTypes/WinSQL/WinSqlBinding.cs index 673f7b1d..ff7d8a80 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/WinSqlBinding.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/WinSqlBinding.cs @@ -57,23 +57,23 @@ public static bool BindSQLCertificate(PSHelper psHelper, string SQLInstanceNames parameters["RestartService"] = restartSQLService; } - _results = psHelper.ExecutePowerShell("Bind-KFSqlCertificate", parameters); + _results = psHelper.ExecutePowerShell("New-KeyfactorSQLBinding", parameters); if (_results != null && _results.Count > 0) { // Extract value from PSObject and convert to bool if (bool.TryParse(_results[0]?.BaseObject?.ToString(), out bool result)) { - _logger.LogTrace($"PowerShell function Bind-KFSqlCertificate returned: {result}"); + _logger.LogTrace($"PowerShell function New-KeyfactorSQLBinding returned: {result}"); return result; } } - _logger.LogWarning("PowerShell function Bind-KFSqlCertificate did not return a valid boolean result."); + _logger.LogWarning("PowerShell function New-KeyfactorSQLBinding did not return a valid boolean result."); return false; } catch (Exception ex) { - _logger.LogError(ex, "Error executing PowerShell function: Bind-KFSqlCertificate"); + _logger.LogError(ex, "Error executing PowerShell function: New-KeyfactorSQLBinding"); return false; } } @@ -96,22 +96,22 @@ public static bool UnBindSQLCertificate(PSHelper psHelper, string SQLInstanceNam parameters["RestartService"] = restartSQLService; } - _results = psHelper.ExecutePowerShell("Unbind-KFSqlCertificate", parameters); + _results = psHelper.ExecutePowerShell("Remove-KeyfactorSQLCertificate", parameters); if (_results != null && _results.Count > 0) { if (bool.TryParse(_results[0]?.BaseObject?.ToString(), out bool result)) { - _logger.LogTrace($"PowerShell function Unbind-KFSqlCertificate returned: {result}"); + _logger.LogTrace($"PowerShell function Remove-KeyfactorSQLCertificate returned: {result}"); return result; } } - _logger.LogWarning("PowerShell function Unbind-KFSqlCertificate did not return a valid boolean result."); + _logger.LogWarning("PowerShell function Remove-KeyfactorSQLCertificate did not return a valid boolean result."); return false; } catch (Exception ex) { - _logger.LogError(ex, "Error occurred while unbinding certificate(s) from SQL instance(s)"); + _logger.LogError(ex, "Error executing PowerShell function: Remove-KeyfactorSQLCertificate"); return false; } } diff --git a/IISU/Models/JobProperties.cs b/IISU/Models/JobProperties.cs index 3d534e78..d9d70747 100644 --- a/IISU/Models/JobProperties.cs +++ b/IISU/Models/JobProperties.cs @@ -52,12 +52,8 @@ public JobProperties() [DefaultValue(true)] public bool RestartService { get; set; } - [JsonProperty("UseJEA")] - [DefaultValue(false)] - public bool UseJEA { get; set; } - [JsonProperty("JEAEndpointName")] - [DefaultValue("keyfactor.wincert")] - public string JEAEndpointName { get; set; } = "keyfactor.wincert"; + [DefaultValue("")] + public string JEAEndpointName { get; set; } = ""; } } \ No newline at end of file diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 3fbdac3c..1337b4cd 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -60,8 +60,8 @@ public class PSHelper : IDisposable private bool isLocalMachine; private bool isADFSStore = false; - private bool useJea = false; - private string jeaEndpoint = "keyfactor.wincert"; + private string jeaEndpoint = ""; + private bool useJea => !string.IsNullOrEmpty(jeaEndpoint); public bool IsLocalMachine { @@ -97,7 +97,7 @@ public PSHelper() _logger = LogHandler.GetClassLogger(); } - public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false, bool useJea = false, string jeaEndpoint = "keyfactor.wincert") + public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false, string jeaEndpoint = "") { this.protocol = protocol.ToLower(); this.port = port; @@ -106,7 +106,6 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN this.serverUserName = serverUserName; this.serverPassword = serverPassword; this.isADFSStore = isADFSStore; - this.useJea = useJea; this.jeaEndpoint = jeaEndpoint; _logger = LogHandler.GetClassLogger(); @@ -115,7 +114,7 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN _logger.LogTrace($"Port: {this.port}"); _logger.LogTrace($"UseSPN: {this.useSPN}"); _logger.LogTrace($"ClientMachineName: {ClientMachineName}"); - _logger.LogTrace($"UseJEA: {this.useJea}"); + _logger.LogTrace($"JEA Active: {this.useJea}"); _logger.LogTrace($"JEAEndpoint: {this.jeaEndpoint}"); _logger.LogTrace("Constructor Completed"); } @@ -280,6 +279,29 @@ private void InitializeRemoteSession() else { _logger.LogDebug($"JEA session active on endpoint '{jeaEndpoint}' - skipping script injection, functions are pre-registered."); + + // Pre-flight: verify Keyfactor modules are installed on the JEA endpoint. + PS.AddCommand("Invoke-Command") + .AddParameter("Session", _PSSession) + .AddParameter("ScriptBlock", ScriptBlock.Create("[bool](Get-Command 'New-KeyfactorResult' -ErrorAction SilentlyContinue)")); + var preFlightResults = PS.Invoke(); + PS.Commands.Clear(); + + bool modulesInstalled = preFlightResults != null && + preFlightResults.Count > 0 && + preFlightResults[0]?.BaseObject is bool preFlightBool && + preFlightBool; + + if (!modulesInstalled) + { + throw new Exception( + $"JEA endpoint '{jeaEndpoint}' is reachable but Keyfactor modules are not installed. " + + "Install Keyfactor.WinCert.Common (and any required store-type modules) under " + + "'C:\\Program Files\\WindowsPowerShell\\Modules\\' on the target machine, " + + "re-register the JEA session configuration, and restart WinRM."); + } + + _logger.LogDebug("JEA pre-flight passed: Keyfactor modules are installed on the endpoint."); } // Set $InformationPreference globally so Write-Information output is forwarded @@ -918,8 +940,11 @@ public static void ProcessPowerShellScriptEvent(object? sender, DataAddedEventAr var infoMessages = sender as PSDataCollection; if (infoMessages != null) { - var infoMessage = infoMessages[e.Index]; - _logger.LogInformation($"INFO: {infoMessage.MessageData}"); + var msg = infoMessages[e.Index].MessageData?.ToString() ?? string.Empty; + if (msg.StartsWith("[VERBOSE] ", StringComparison.Ordinal)) + _logger.LogTrace("{Message}", msg[10..]); + else + _logger.LogInformation("INFO: {Message}", msg); } break; diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc index 80d1cbd5..5dc01f51 100644 --- a/IISU/PowerShell/Build/KeyfactorWinCert.pssc +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -11,6 +11,7 @@ # $base = 'C:\Program Files\WindowsPowerShell\Modules' # Copy-Item -Path '.\Keyfactor.WinCert.Common' -Destination "$base\Keyfactor.WinCert.Common" -Recurse -Force # Copy-Item -Path '.\Keyfactor.WinCert.IIS' -Destination "$base\Keyfactor.WinCert.IIS" -Recurse -Force +# Copy-Item -Path '.\Keyfactor.WinCert.SQL' -Destination "$base\Keyfactor.WinCert.SQL" -Recurse -Force # # (Only install the modules needed for the store types you use on this endpoint.) # @@ -91,9 +92,11 @@ # Multiple capabilities are merged — the session exposes all their VisibleFunctions. # # Examples: - # WinCert + IIS: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } - # WinCert only: @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } + # WinCert + IIS: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } + # WinCert + SQL: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL' } + # WinCert + IIS + SQL: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } + # WinCert only: @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } RoleDefinitions = @{ - 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } + 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } } } diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 index 45fec70d..8b80e7f8 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 @@ -10,12 +10,14 @@ . "$PSScriptRoot\Private\Get-CryptoProviders.ps1" . "$PSScriptRoot\Private\Test-CryptoServiceProvider.ps1" . "$PSScriptRoot\Private\Validate-CryptoProvider.ps1" +. "$PSScriptRoot\Private\Convert-DNSSubject.ps1" # Public functions . "$PSScriptRoot\Public\New-KeyfactorResult.ps1" . "$PSScriptRoot\Public\Get-KeyfactorCertificates.ps1" . "$PSScriptRoot\Public\Add-KeyfactorCertificate.ps1" . "$PSScriptRoot\Public\Remove-KeyfactorCertificate.ps1" +. "$PSScriptRoot\Public\New-KeyfactorODKGEnrollment.ps1" # Export only public functions for non-JEA use. # In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. @@ -25,6 +27,7 @@ Export-ModuleMember -Function @( 'Get-KeyfactorCertificates', 'Add-KeyfactorCertificate', 'Remove-KeyfactorCertificate', + 'New-KeyfactorODKGEnrollment', # Shared certificate inspection utilities — exported so other modules (e.g. IIS) can call them 'Get-CertificateCSP', 'Get-CertificateSAN' diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Convert-DNSSubject.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Convert-DNSSubject.ps1 new file mode 100644 index 00000000..c2089b7a --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Convert-DNSSubject.ps1 @@ -0,0 +1,103 @@ +function Convert-DNSSubject { + <# + .SYNOPSIS + Parses a Distinguished Name (DN) subject string and properly quotes RDN values containing escaped commas. + + .DESCRIPTION + This function takes a DN subject string and parses the Relative Distinguished Name (RDN) components, + adding proper quotes around values that contain escaped commas and escaping quotes for use in + PowerShell here-strings. Only RDN values with escaped commas get quoted. + + .PARAMETER Subject + The DN subject string to parse (e.g., "CN=Keyfactor,O=Keyfactor\, Inc") + + .EXAMPLE + Convert-DNSSubject -Subject "CN=Keyfactor,O=Keyfactor\, Inc" + Returns: CN=Keyfactor,O=""Keyfactor, Inc"" + + .EXAMPLE + Convert-DNSSubject -Subject "CN=Test User,O=Company\, LLC,OU=IT Department\, Security" + Returns: CN=Test User,O=""Company, LLC"",OU=""IT Department, Security"" + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Subject + ) + + # Initialize variables + $parsedComponents = @() + $currentComponent = "" + $i = 0 + + # Convert string to character array for easier parsing + $chars = $Subject.ToCharArray() + + while ($i -lt $chars.Length) { + $char = $chars[$i] + + # Check if we hit a comma + if ($char -eq ',') { + # Look back to see if it's escaped + $isEscaped = $false + if ($i -gt 0 -and $chars[$i-1] -eq '\') { + $isEscaped = $true + } + + if ($isEscaped) { + # This is an escaped comma, add it to current component + $currentComponent += $char + } else { + # This is a separator comma, finish current component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + $currentComponent = "" + } + } + } else { + # Regular character, add to current component + $currentComponent += $char + } + + $i++ + } + + # Add the last component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + } + + # Process each component to add quotes where needed + $processedComponents = @() + + foreach ($component in $parsedComponents) { + # Split on first equals sign to get attribute and value + $equalIndex = $component.IndexOf('=') + if ($equalIndex -gt 0) { + $attribute = $component.Substring(0, $equalIndex).Trim() + $value = $component.Substring($equalIndex + 1).Trim() + + # Clean up escaped commas first + $cleanValue = $value -replace '\\,', ',' + + # Check if original value had escaped commas (needs quotes) + if ($value -match '\\,') { + # This RDN value had escaped commas, so wrap in doubled quotes and escape quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=`"`"$escapedValue`"`"" + } else { + # No escaped commas, keep as simple value but escape any existing quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=$escapedValue" + } + } else { + # Invalid component format, keep as is + $processedComponents += $component + } + } + + # Join components back together (no outer quotes needed since it goes in PowerShell string) + $subjectString = ($processedComponents -join ',') + return $subjectString +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 index e10ba5bc..308811cb 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CertificateCSP.ps1 @@ -1,4 +1,4 @@ -function Get-CertificateCSP +function Get-CertificateCSP { param( [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert @@ -17,7 +17,7 @@ function Get-CertificateCSP } } catch { - Write-Verbose "CNG provider lookup failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] CNG provider lookup failed: $($_.Exception.Message)" } return $null } @@ -45,7 +45,7 @@ function Get-CertificateCSP } } catch { - Write-Verbose "RSA CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] RSA CNG detection failed: $($_.Exception.Message)" } # ── 3. ECC / ECDsa (ECDsaCng) ───────────────────────────────────────── @@ -56,12 +56,12 @@ function Get-CertificateCSP $providerName = Get-CngProviderName $ecKey if ($providerName) { return $providerName } - Write-Verbose "ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" + Write-Information "[VERBOSE] ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" return "" } } catch { - Write-Verbose "ECDsa CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] ECDsa CNG detection failed: $($_.Exception.Message)" } # ── 4. DSA (bonus) ──────────────────────────────────────────────────── @@ -71,15 +71,15 @@ function Get-CertificateCSP $providerName = Get-CngProviderName $dsaKey if ($providerName) { return $providerName } - Write-Verbose "DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" + Write-Information "[VERBOSE] DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" return "" } } catch { - Write-Verbose "DSA CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] DSA CNG detection failed: $($_.Exception.Message)" } - Write-Verbose "No supported key type detected; provider name could not be determined" + Write-Information "[VERBOSE] No supported key type detected; provider name could not be determined" return "" } catch { diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 index b8257337..bf04e586 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 @@ -1,7 +1,7 @@ -function Get-CryptoProviders { +function Get-CryptoProviders { # Retrieves the list of available Crypto Service Providers using certutil try { - Write-Verbose "Retrieving Crypto Service Providers using certutil..." + Write-Information "[VERBOSE] Retrieving Crypto Service Providers using certutil..." $certUtilOutput = certutil -csplist # Parse the output to extract CSP names @@ -17,8 +17,8 @@ function Get-CryptoProviders { throw "No Crypto Service Providers were found. Ensure certutil is functioning properly." } - Write-Verbose "Retrieved the following CSPs:" - $cspInfoList | ForEach-Object { Write-Verbose $_ } + Write-Information "[VERBOSE] Retrieved the following CSPs:" + $cspInfoList | ForEach-Object { Write-Information "[VERBOSE] $_" } return $cspInfoList } catch { diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 index 4991205e..05d3adf6 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1 @@ -1,9 +1,9 @@ -function Validate-CryptoProvider { +function Validate-CryptoProvider { param ( [Parameter(Mandatory)] [string]$ProviderName ) - Write-Verbose "Validating CSP: $ProviderName" + Write-Information "[VERBOSE] Validating CSP: $ProviderName" $availableProviders = Get-CryptoProviders @@ -12,5 +12,5 @@ function Validate-CryptoProvider { throw "Crypto Service Provider '$ProviderName' is either invalid or not found on this system." } - Write-Verbose "Crypto Service Provider '$ProviderName' is valid." + Write-Information "[VERBOSE] Crypto Service Provider '$ProviderName' is valid." } \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 index ed44b4f0..1cf00d67 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 @@ -1,4 +1,4 @@ -function Add-KeyfactorCertificate { +function Add-KeyfactorCertificate { param ( [Parameter(Mandatory = $true)] [string]$Base64Cert, @@ -15,7 +15,7 @@ function Add-KeyfactorCertificate { try { Write-Information "Entering PowerShell Script Add-KeyfactorCertificateToStore" - Write-Verbose "Add-KeyfactorCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" + Write-Information "[VERBOSE] Add-KeyfactorCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" # Get the thumbprint of the passed in certificate # Convert password to secure string if provided, otherwise use $null @@ -54,13 +54,13 @@ function Add-KeyfactorCertificate { $arguments = @('-f') if ($PrivateKeyPassword) { - Write-Verbose "Has a private key" + Write-Information "[VERBOSE] Has a private key" $arguments += '-p' $arguments += $PrivateKeyPassword } if ($CryptoServiceProvider) { - Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider" + Write-Information "[VERBOSE] Has a CryptoServiceProvider: $CryptoServiceProvider" $arguments += '-csp' $arguments += $CryptoServiceProvider } @@ -74,7 +74,7 @@ function Add-KeyfactorCertificate { if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } }) -join ' ' - write-Verbose "Running certutil with arguments: $argLine" + Write-Information "[VERBOSE] Running certutil with arguments: $argLine" # Setup process execution $processInfo = New-Object System.Diagnostics.ProcessStartInfo diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorODKGEnrollment.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorODKGEnrollment.ps1 new file mode 100644 index 00000000..f6c9969f --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorODKGEnrollment.ps1 @@ -0,0 +1,97 @@ +function New-KeyfactorODKGEnrollment { + param ( + [string]$SubjectText, + [string]$ProviderName = "Microsoft Strong Cryptographic Provider", + [string]$KeyType, + [string]$KeyLength, + [string]$SAN + ) + + if ([string]::IsNullOrWhiteSpace($ProviderName)) { + $ProviderName = "Microsoft Strong Cryptographic Provider" + } + + # Validate the Crypto Service Provider + Validate-CryptoProvider -ProviderName $ProviderName + + # Parse Subject for any escaped commas + $parsedSubject = Convert-DNSSubject -Subject $SubjectText + + # Build the SAN entries if provided + $sanContent = "" + if ($SAN) { + $sanEntries = $SAN -split "&" + $sanDirectives = $sanEntries | ForEach-Object { "_continue_ = `"$($_)&`"" } + $sanContent = @" +[Extensions] +2.5.29.17 = `"{text}`" +$($sanDirectives -join "`n") +"@ + } + + # Generate INF file content for the CSR + $infContent = @" +[Version] +Signature=`"$`Windows NT$`" + +[NewRequest] +Subject = "$parsedSubject" +ProviderName = "$ProviderName" +MachineKeySet = True +HashAlgorithm = SHA256 +KeyAlgorithm = $KeyType +KeyLength = $KeyLength +KeySpec = 0 + +$sanContent +"@ + + Write-Information "[VERBOSE] INF Contents: $infContent" + + # Path to temporary INF file + $infFile = [System.IO.Path]::GetTempFileName() + ".inf" + $csrOutputFile = [System.IO.Path]::GetTempFileName() + ".csr" + + Set-Content -Path $infFile -Value $infContent + Write-Information "Generated INF file at: $infFile" + + try { + # Run certreq to generate CSR + $certReqCommand = "certreq -new -q `"$infFile`" `"$csrOutputFile`"" + Write-Information "Running certreq: $certReqCommand" + + # Capture the output and errors + $certReqOutput = & certreq -new -q $infFile $csrOutputFile 2>&1 + + # Check the exit code of the command + if ($LASTEXITCODE -ne 0) { + $errMsg = "Certreq failed with exit code $LASTEXITCODE. Output: $certReqOutput" + throw $errMsg + } + + # If successful, proceed + Write-Information "Certreq completed successfully." + + # Read CSR file + if (Test-Path $csrOutputFile) { + $csrContent = Get-Content -Path $csrOutputFile -Raw + Write-Information "CSR successfully created at: $csrOutputFile" + return $csrContent + } else { + throw "Failed to create CSR file." + } + } catch { + Write-Error $_ + } finally { + # Clean up temporary files + if (Test-Path $infFile) { + Remove-Item -Path $infFile -Force + Write-Information "Deleted temporary INF file." + } + + if (Test-Path $csrOutputFile) { + Remove-Item -Path $csrOutputFile -Force + Write-Information "Deleted temporary CSR file." + } + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc index 875fd41c..4450cee3 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc @@ -36,6 +36,7 @@ 'Add-KeyfactorCertificate', 'Remove-KeyfactorCertificate', 'New-KeyfactorResult', + 'New-KeyfactorODKGEnrollment', 'Get-CertificateCSP', 'Get-CertificateSAN' ) @@ -53,10 +54,12 @@ ) # certutil.exe is called directly via the PowerShell pipeline by Get-CryptoProviders. - # Full path is required. Add-KeyfactorCertificate uses certutil via .NET ProcessStartInfo + # certreq.exe is called directly via the PowerShell pipeline by New-KeyfactorODKGEnrollment. + # Full path is required for both. Add-KeyfactorCertificate uses certutil via .NET ProcessStartInfo # and does NOT need it listed here. VisibleExternalCommands = @( - 'C:\Windows\System32\certutil.exe' + 'C:\Windows\System32\certutil.exe', + 'C:\Windows\System32\certreq.exe' ) # No variables need to be pre-defined for the orchestrator. diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 index 8ba77bee..5669de68 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Add-IISBindingWithSSL.ps1 @@ -1,4 +1,4 @@ -function Add-IISBindingWithSSL { +function Add-IISBindingWithSSL { [CmdletBinding()] [OutputType([pscustomobject])] param ( @@ -28,7 +28,7 @@ function Add-IISBindingWithSSL { [bool]$UseIISDrive ) - Write-Verbose "Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" + Write-Information "[VERBOSE] Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" try { if ($UseIISDrive) { @@ -46,22 +46,22 @@ function Add-IISBindingWithSSL { $bindingParams.HostHeader = $Hostname } - Write-Verbose "Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" + Write-Information "[VERBOSE] Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" New-WebBinding @bindingParams # Bind SSL certificate if HTTPS if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Verbose "Searching for binding: $searchBindings" + Write-Information "[VERBOSE] Searching for binding: $searchBindings" $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { $_.bindingInformation -eq $searchBindings } if ($binding) { - Write-Verbose "Binding SSL certificate with thumbprint: $Thumbprint" + Write-Information "[VERBOSE] Binding SSL certificate with thumbprint: $Thumbprint" $null = $binding.AddSslCertificate($Thumbprint, $StoreName) - Write-Verbose "SSL certificate successfully bound" + Write-Information "[VERBOSE] SSL certificate successfully bound" return New-KeyfactorResult -Status Success -Code 0 -Step BindSSL -Message "Binding and SSL certificate successfully applied" } else { return New-KeyfactorResult -Status Error -Code 202 -Step BindSSL -ErrorMessage "No binding found for: $searchBindings" diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 index ded1400b..08707bc8 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-IISManagementInfo.ps1 @@ -1,4 +1,4 @@ -function Get-IISManagementInfo { +function Get-IISManagementInfo { [CmdletBinding()] [OutputType([hashtable])] param ( @@ -7,7 +7,7 @@ function Get-IISManagementInfo { ) $hasIISDrive = Test-IISDrive - Write-Verbose "IIS Drive available: $hasIISDrive" + Write-Information "[VERBOSE] IIS Drive available: $hasIISDrive" if ($hasIISDrive) { $null = Import-Module WebAdministration diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 index f92bd6db..b5c5d347 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Get-ValidSslFlagsForSystem.ps1 @@ -1,4 +1,4 @@ -function Get-ValidSslFlagsForSystem { +function Get-ValidSslFlagsForSystem { <# .SYNOPSIS Gets the valid SSL flag bits for the current Windows Server version @@ -11,22 +11,22 @@ function Get-ValidSslFlagsForSystem { # Return array of valid flag values based on Windows Server version if ($build -ge 20348) { # Windows Server 2022+ (IIS 10.0.20348+) - Write-Verbose "Detected Windows Server 2022 or later (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2022 or later (Build: $build)" return @(1, 4, 8, 16, 32, 64) # Include unknowns for testing } elseif ($build -ge 17763) { # Windows Server 2019 (IIS 10.0.17763) - Write-Verbose "Detected Windows Server 2019 (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2019 (Build: $build)" return @(1, 4, 8) } elseif ($build -ge 14393) { # Windows Server 2016 (IIS 10.0) - Write-Verbose "Detected Windows Server 2016 (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2016 (Build: $build)" return @(1, 4) } else { # Windows Server 2012 R2 and earlier (IIS 8.5) - Write-Verbose "Detected Windows Server 2012 R2 or earlier (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2012 R2 or earlier (Build: $build)" return @(1, 2) } } \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 index c0762980..dfe10f3d 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Test-ValidSslFlags.ps1 @@ -1,4 +1,4 @@ -function Test-ValidSslFlags { +function Test-ValidSslFlags { [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -66,7 +66,7 @@ function Test-ValidSslFlags { $successMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is valid for this system (Build: $build)." if ($ThrowOnError) { - Write-Verbose $successMsg + Write-Information "[VERBOSE] $successMsg" return $true } else { diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 index 2de40982..dddfe8d1 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 @@ -1,4 +1,4 @@ -function New-KeyfactorIISSiteBinding { +function New-KeyfactorIISSiteBinding { [CmdletBinding()] [OutputType([pscustomobject])] param ( @@ -22,7 +22,7 @@ function New-KeyfactorIISSiteBinding { ) Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue - Write-Verbose "Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" + Write-Information "[VERBOSE] Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" try { # Step 1: Perform verifications and get management info @@ -39,7 +39,7 @@ function New-KeyfactorIISSiteBinding { # Step 2: Remove existing HTTPS bindings for this binding info $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Verbose "Removing existing HTTPS bindings for: $searchBindings" + Write-Information "[VERBOSE] Removing existing HTTPS bindings for: $searchBindings" $removalResult = Remove-KeyfactorIISSiteBinding -SiteName $SiteName -BindingInfo $searchBindings -UseIISDrive $managementInfo.UseIISDrive if ($removalResult.Status -eq 'Error') { @@ -48,14 +48,14 @@ function New-KeyfactorIISSiteBinding { # Step 3: Determine SslFlags supported by Microsoft.Web.Administration if ($SslFlags -gt 3) { - Write-Verbose "SslFlags value $SslFlags exceeds managed API range (0-3). Applying reduced flags for creation." + Write-Information "[VERBOSE] SslFlags value $SslFlags exceeds managed API range (0-3). Applying reduced flags for creation." $SslFlagsApplied = ($SslFlags -band 3) } else { $SslFlagsApplied = $SslFlags } # Step 4: Add the new binding with the reduced flag set - Write-Verbose "Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" + Write-Information "[VERBOSE] Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" $addParams = @{ SiteName = $SiteName @@ -77,7 +77,7 @@ function New-KeyfactorIISSiteBinding { # Step 5: If extended flags, update via appcmd.exe if ($SslFlags -gt 3) { - Write-Verbose "Applying full SslFlags=$SslFlags via appcmd" + Write-Information "[VERBOSE] Applying full SslFlags=$SslFlags via appcmd" $appcmd = Join-Path $env:windir "System32\inetsrv\appcmd.exe" @@ -95,16 +95,16 @@ function New-KeyfactorIISSiteBinding { # Build binding argument for appcmd $bindingArg = "/bindings.[protocol='https',bindingInformation='$bindingInfo'].sslFlags:$SslFlags" - Write-Verbose "Running appcmd: $appcmd $siteArg $bindingArg" + Write-Information "[VERBOSE] Running appcmd: $appcmd $siteArg $bindingArg" $appcmdOutput = & $appcmd set site $siteArg $bindingArg 2>&1 - Write-Verbose "appcmd output: $appcmdOutput" + Write-Information "[VERBOSE] appcmd output: $appcmdOutput" #& $appcmd set site $siteArg $bindingArg | Out-Null if ($LASTEXITCODE -ne 0) { Write-Warning "appcmd failed to set extended SslFlags ($SslFlags) for binding $bindingInfo." } else { - Write-Verbose "Successfully updated SslFlags to $SslFlags via appcmd." + Write-Information "[VERBOSE] Successfully updated SslFlags to $SslFlags via appcmd." } } diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 index baf0a150..28a903d2 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 @@ -1,4 +1,4 @@ -function Remove-KeyfactorIISSiteBinding { +function Remove-KeyfactorIISSiteBinding { [CmdletBinding()] [OutputType([pscustomobject])] param ( @@ -18,11 +18,11 @@ function Remove-KeyfactorIISSiteBinding { $UseIISDrive = Test-IISDrive } - Write-Verbose "Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" + Write-Information "[VERBOSE] Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" try { if ($UseIISDrive) { - Write-Verbose "Using IIS Drive to remove binding" + Write-Information "[VERBOSE] Using IIS Drive to remove binding" $sitePath = "IIS:\Sites\$SiteName" $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { @@ -33,13 +33,13 @@ function Remove-KeyfactorIISSiteBinding { $bindingInfo = $binding.GetAttributeValue("bindingInformation") $protocol = $binding.protocol - Write-Verbose "Removing binding: $bindingInfo ($protocol)" + Write-Information "[VERBOSE] Removing binding: $bindingInfo ($protocol)" Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false - Write-Verbose "Successfully removed binding" + Write-Information "[VERBOSE] Successfully removed binding" } } else { - Write-Verbose "Using Web Administration assembly to remove binding" + Write-Information "[VERBOSE] Using Web Administration assembly to remove binding" # ServerManager fallback Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" $iis = New-Object Microsoft.Web.Administration.ServerManager @@ -50,13 +50,13 @@ function Remove-KeyfactorIISSiteBinding { } foreach ($binding in $httpsBindings) { - Write-Verbose "Removing binding: $($binding.BindingInformation)" + Write-Information "[VERBOSE] Removing binding: $($binding.BindingInformation)" $site.Bindings.Remove($binding) - Write-Verbose "Successfully removed binding" + Write-Information "[VERBOSE] Successfully removed binding" } $iis.CommitChanges() - Write-Verbose "Committed changes to IIS" + Write-Information "[VERBOSE] Committed changes to IIS" } return New-KeyfactorResult -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Keyfactor.WinCert.SQL.psm1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Keyfactor.WinCert.SQL.psm1 new file mode 100644 index 00000000..251d3d3d --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Keyfactor.WinCert.SQL.psm1 @@ -0,0 +1,29 @@ +# Keyfactor.WinCert.SQL.psm1 +# +# Static explicit dot-sourcing is used instead of Get-ChildItem/Select-Object discovery. +# This avoids a dependency on proxy-restricted cmdlets during JEA session initialization. + +# Load Keyfactor.WinCert.Common if not already loaded (non-JEA local sessions). +# In JEA sessions the .psrc ModulesToImport loads both modules before any function is called. +if (-not (Get-Command 'New-KeyfactorResult' -ErrorAction SilentlyContinue)) { + $commonModulePath = Join-Path $PSScriptRoot '..\Keyfactor.WinCert.Common\Keyfactor.WinCert.Common.psm1' + if (Test-Path $commonModulePath) { Import-Module $commonModulePath -Force } +} + +# Private helpers must be loaded before the public functions that call them. +. "$PSScriptRoot\Private\Get-KeyfactorSQLServiceName.ps1" +. "$PSScriptRoot\Private\Get-KeyfactorSQLServiceUser.ps1" +. "$PSScriptRoot\Private\Set-KeyfactorSQLCertificateBinding.ps1" + +# Public functions +. "$PSScriptRoot\Public\Get-KeyfactorSQLInventory.ps1" +. "$PSScriptRoot\Public\New-KeyfactorSQLBinding.ps1" +. "$PSScriptRoot\Public\Remove-KeyfactorSQLCertificate.ps1" + +# Export only public functions for non-JEA use. +# In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. +Export-ModuleMember -Function @( + 'Get-KeyfactorSQLInventory', + 'New-KeyfactorSQLBinding', + 'Remove-KeyfactorSQLCertificate' +) diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceName.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceName.ps1 new file mode 100644 index 00000000..95aa6aff --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceName.ps1 @@ -0,0 +1,10 @@ +function Get-KeyfactorSQLServiceName { + param ( + [string]$InstanceName + ) + if ($InstanceName -eq "MSSQLSERVER") { + return "MSSQLSERVER" # Default instance + } else { + return "MSSQL`$$InstanceName" # Named instance (escape $ for PowerShell strings) + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 new file mode 100644 index 00000000..32649e79 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 @@ -0,0 +1,19 @@ +function Get-KeyfactorSQLServiceUser { + param ( + [Parameter(Mandatory = $true)] + [string]$SQLServiceName + ) + + # Use Get-CimInstance instead of Get-WmiObject + $serviceUser = (Get-CimInstance -ClassName Win32_Service -Filter "Name='$SQLServiceName'").StartName + + if ($serviceUser) { + return $serviceUser + } else { + Write-Error "SQL Server instance '$SQLInstanceName' not found or no service user associated." + return $null + } + +# Example usage: +# Get-SQLServiceUser -SQLInstanceName "MSSQLSERVER" +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 new file mode 100644 index 00000000..d593668b --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 @@ -0,0 +1,547 @@ +function Set-KeyfactorSQLCertificateBinding { + <# + .SYNOPSIS + Binds a certificate to a SQL Server instance and sets appropriate permissions. + + .DESCRIPTION + This function binds a certificate to a SQL Server instance by updating the registry, + setting ACL permissions on the private key, and optionally restarting the SQL service. + Supports both local Windows-to-Windows and SSH-based remote connections. + Handles both CNG (modern) and CAPI (legacy) certificate key storage. + + .PARAMETER InstanceName + The SQL Server instance name (e.g., "MSSQLSERVER" for default instance) + + .PARAMETER NewThumbprint + The thumbprint of the certificate to bind + + .PARAMETER RestartService + Switch to restart the SQL Server service after binding + + .EXAMPLE + Set-KFSQLCertificateBinding -InstanceName "MSSQLSERVER" -NewThumbprint "ABC123..." -RestartService + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$InstanceName, + + [Parameter(Mandatory = $true)] + [string]$NewThumbprint, + + [switch]$RestartService + ) + + Write-Information "Starting certificate binding process for instance: $InstanceName" + Write-Information "Target certificate thumbprint: $NewThumbprint" + + try { + # ============================================================ + # STEP 1: Get SQL Instance Registry Path + # ============================================================ + Write-Information "Retrieving SQL Server instance information..." + + try { + $fullInstance = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $InstanceName -ErrorAction Stop + $RegistryPath = Get-SqlCertRegistryLocation -InstanceName $fullInstance + + Write-Information "[VERBOSE] Full instance name: $fullInstance" + Write-Information "[VERBOSE] Registry path: $RegistryPath" + Write-Information "SQL Server instance registry path located: $RegistryPath" + } + catch { + Write-Error "Failed to locate SQL Server instance '$InstanceName' in registry: $_" + throw $_ + } + + # ============================================================ + # STEP 2: Update Registry with New Certificate Thumbprint + # ============================================================ + Write-Information "Updating registry with new certificate thumbprint..." + + try { + # Backup current value + $currentThumbprint = Get-ItemPropertyValue -Path $RegistryPath -Name "Certificate" -ErrorAction SilentlyContinue + + if ($currentThumbprint) { + Write-Information "[VERBOSE] Current certificate thumbprint: $currentThumbprint" + } else { + Write-Information "[VERBOSE] No existing certificate thumbprint found" + } + + # Set new thumbprint + Set-ItemProperty -Path $RegistryPath -Name "Certificate" -Value $NewThumbprint -ErrorAction Stop + + # Verify the change + $verifyThumbprint = Get-ItemPropertyValue -Path $RegistryPath -Name "Certificate" -ErrorAction Stop + + if ($verifyThumbprint -eq $NewThumbprint) { + Write-Information "Registry updated successfully" + } else { + throw "Registry update verification failed. Expected: $NewThumbprint, Got: $verifyThumbprint" + } + } + catch { + Write-Error "Failed to update registry at '$RegistryPath': $_" + throw $_ + } + + # ============================================================ + # STEP 3: Get SQL Server Service Information + # ============================================================ + Write-Information "Retrieving SQL Server service information..." + + try { + $serviceName = Get-KeyfactorSQLServiceName -InstanceName $InstanceName + $serviceInfo = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction Stop + $SqlServiceUser = $serviceInfo.StartName + + if (-not $SqlServiceUser) { + throw "Unable to retrieve service account for SQL Server instance: $InstanceName" + } + + # Normalize service account name for ACL operations + if ($SqlServiceUser -eq "LocalSystem") { + $SqlServiceUser = "NT AUTHORITY\SYSTEM" + Write-Information "[VERBOSE] Normalized LocalSystem to: $SqlServiceUser" + } + elseif ($SqlServiceUser -match "^NT Service\\") { + # NT Service accounts are already in correct format + Write-Information "[VERBOSE] Using NT Service account: $SqlServiceUser" + } + elseif ($SqlServiceUser.StartsWith(".\")) { + # Local account - convert to machine\user format + $SqlServiceUser = "$env:COMPUTERNAME$($SqlServiceUser.Substring(1))" + Write-Information "[VERBOSE] Normalized local account to: $SqlServiceUser" + } + + Write-Information "[VERBOSE] Service name: $serviceName" + Write-Information "SQL Server service account: $SqlServiceUser" + } + catch { + Write-Error "Failed to retrieve SQL Server service information: $_" + throw $_ + } + + # ============================================================ + # STEP 4: Locate Certificate and Private Key + # ============================================================ + Write-Information "Locating certificate and private key..." + + try { + # Get the certificate from the LocalMachine\My store + $Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $NewThumbprint } + + if (-not $Cert) { + throw "Certificate with thumbprint $NewThumbprint not found in LocalMachine\My store" + } + + Write-Information "[VERBOSE] Certificate found: $($Cert.Subject)" + + if (-not $Cert.HasPrivateKey) { + throw "Certificate does not have a private key" + } + + # Detect private key location (CNG vs CAPI) + $privKeyPath = $null + $privKey = $null + $keyStorageType = $null + + # Try CNG first (modern certificates) + try { + $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Cert) + + if ($rsaKey -is [System.Security.Cryptography.RSACng]) { + $privKey = $rsaKey.Key.UniqueName + $keyStorageType = "CNG" + Write-Information "[VERBOSE] Certificate uses CNG key storage" + Write-Information "[VERBOSE] CNG key unique name: $privKey" + + # CNG keys can be in multiple locations - check them all + $possiblePaths = @( + "$($env:ProgramData)\Microsoft\Crypto\Keys\$privKey", + "$($env:ProgramData)\Microsoft\Crypto\SystemKeys\$privKey", + "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\$privKey" + ) + + Write-Information "[VERBOSE] Searching for CNG private key in known locations..." + foreach ($path in $possiblePaths) { + Write-Information "[VERBOSE] Checking: $path" + if (Test-Path $path) { + $privKeyPath = Get-Item $path -ErrorAction Stop + Write-Information "[VERBOSE] Found CNG private key at: $path" + break + } + } + + # If not found in standard locations, search more broadly + if (-not $privKeyPath) { + Write-Information "[VERBOSE] Key not found in standard locations. Searching all Crypto directories..." + + $searchPaths = @( + "$($env:ProgramData)\Microsoft\Crypto\Keys", + "$($env:ProgramData)\Microsoft\Crypto\SystemKeys", + "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys" + ) + + foreach ($searchPath in $searchPaths) { + if (Test-Path $searchPath) { + $found = Get-ChildItem -Path $searchPath -Filter "*$privKey*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + $privKeyPath = $found + Write-Information "[VERBOSE] Found CNG private key at: $($privKeyPath.FullName)" + break + } + } + } + } + + if ($privKeyPath) { + Write-Information "Certificate uses CNG (Cryptography Next Generation) key storage" + } + } + } + catch { + Write-Information "[VERBOSE] CNG key detection failed or not applicable: $_" + Write-Information "[VERBOSE] Exception type: $($_.Exception.GetType().FullName)" + } + + # Fallback to CAPI/CSP (legacy certificates) + if (-not $privKey -or -not $privKeyPath) { + Write-Information "[VERBOSE] Attempting CAPI/CSP key detection..." + + try { + if ($Cert.PrivateKey -and $Cert.PrivateKey.CspKeyContainerInfo) { + $privKey = $Cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyPath = "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\" + $keyStorageType = "CAPI/CSP" + + Write-Information "[VERBOSE] CAPI/CSP key unique name: $privKey" + Write-Information "[VERBOSE] Expected path: $keyPath$privKey" + + $privKeyPath = Get-Item "$keyPath\$privKey" -ErrorAction Stop + Write-Information "Certificate uses CAPI/CSP (legacy) key storage" + } + } + catch { + Write-Information "[VERBOSE] CAPI/CSP key detection failed: $_" + } + } + + if (-not $privKey) { + throw "Unable to locate private key for certificate. The certificate may not have an accessible private key." + } + + if (-not $privKeyPath) { + # Last resort: try to find the key file by searching + Write-Warning "Private key file not found in expected locations. Attempting comprehensive search..." + + $allCryptoPaths = @( + "$($env:ProgramData)\Microsoft\Crypto\Keys", + "$($env:ProgramData)\Microsoft\Crypto\SystemKeys", + "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys" + ) + + foreach ($searchPath in $allCryptoPaths) { + if (Test-Path $searchPath) { + Write-Information "[VERBOSE] Searching in: $searchPath" + $found = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*$privKey*" -or $_.Name -eq $privKey } | + Select-Object -First 1 + + if ($found) { + $privKeyPath = $found + Write-Information "Private key found at: $($privKeyPath.FullName)" + break + } + } + } + + if (-not $privKeyPath) { + throw "Unable to locate private key file for certificate with thumbprint $NewThumbprint. Key name: $privKey" + } + } + + Write-Information "Private key located at: $($privKeyPath.FullName)" + Write-Information "[VERBOSE] Key storage type: $keyStorageType" + Write-Information "[VERBOSE] Key file size: $($privKeyPath.Length) bytes" + + # Verify we can read the key file + try { + $acl = Get-Acl -Path $privKeyPath.FullName -ErrorAction Stop + Write-Information "[VERBOSE] Successfully accessed private key file ACL" + } + catch { + Write-Warning "Could not read ACL from private key file: $_" + } + } + catch { + Write-Error "Failed to locate certificate or private key: $_" + throw $_ + } + + # ============================================================ + # STEP 5: Set ACL Permissions on Private Key + # ============================================================ + Write-Information "Setting ACL permissions on private key for SQL service account..." + + try { + $aclSet = $false + $aclMethod = $null + + # Attempt 1: Try Set-Acl (works in most local scenarios and some SSH sessions) + try { + Write-Information "[VERBOSE] Attempting ACL update using Set-Acl method..." + + $Acl = Get-Acl -Path $privKeyPath -ErrorAction Stop + $Ar = New-Object System.Security.AccessControl.FileSystemAccessRule( + $SqlServiceUser, + "Read", + "Allow" + ) + $Acl.SetAccessRule($Ar) + Set-Acl -Path $privKeyPath.FullName -AclObject $Acl -ErrorAction Stop + + # Verify the ACL was actually set + $verifyAcl = Get-Acl -Path $privKeyPath + $hasPermission = $verifyAcl.Access | Where-Object { + ($_.IdentityReference.Value -eq $SqlServiceUser -or + $_.IdentityReference.Value -like "*$SqlServiceUser*") -and + $_.FileSystemRights -match "Read" + } + + if ($hasPermission) { + Write-Information "ACL updated successfully using Set-Acl method" + $aclSet = $true + $aclMethod = "Set-Acl" + } else { + Write-Warning "Set-Acl completed but verification failed. Permissions may not be set correctly." + } + } + catch { + Write-Warning "Set-Acl method failed: $_" + Write-Information "[VERBOSE] Error details: $($_.Exception.Message)" + } + + # Attempt 2: Use icacls (more reliable in SSH sessions) + if (-not $aclSet) { + Write-Information "[VERBOSE] Attempting ACL update using icacls method..." + + try { + # Execute icacls to grant Read permissions + $icaclsResult = & icacls.exe $privKeyPath.FullName /grant "${SqlServiceUser}:(R)" 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Information "[VERBOSE] icacls command executed successfully" + + # Verify with icacls + $verifyResult = & icacls.exe $privKeyPath.FullName 2>&1 + + if ($verifyResult -match [regex]::Escape($SqlServiceUser)) { + Write-Information "ACL updated successfully using icacls method" + $aclSet = $true + $aclMethod = "icacls" + } else { + Write-Warning "icacls completed but verification failed" + } + } else { + Write-Warning "icacls failed with exit code $LASTEXITCODE" + Write-Information "[VERBOSE] icacls output: $icaclsResult" + } + } + catch { + Write-Warning "icacls method failed: $_" + } + } + + # Attempt 3: Use Scheduled Task (fallback for restricted SSH sessions) + if (-not $aclSet) { + Write-Warning "Standard ACL methods failed. Attempting scheduled task method (elevated privileges)..." + + try { + # Create a temporary script to set the ACL + $tempScriptPath = Join-Path $env:TEMP "SetCertACL_$((Get-Date).Ticks).ps1" + + $scriptContent = @" +try { + `$privKeyPath = '$($privKeyPath.FullName)' + `$SqlServiceUser = '$SqlServiceUser' + + # Try icacls first + `$result = & icacls.exe `$privKeyPath /grant "`${SqlServiceUser}:(R)" 2>&1 + + if (`$LASTEXITCODE -eq 0) { + Set-Content -Path '$env:TEMP\acl_success.txt' -Value "Success via icacls" + } else { + # Fallback to Set-Acl + `$Acl = Get-Acl -Path `$privKeyPath + `$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule( + `$SqlServiceUser, + 'Read', + 'Allow' + ) + `$Acl.SetAccessRule(`$Ar) + Set-Acl -Path `$privKeyPath -AclObject `$Acl + Set-Content -Path '$env:TEMP\acl_success.txt' -Value "Success via Set-Acl" + } +} catch { + Set-Content -Path '$env:TEMP\acl_error.txt' -Value `$_.Exception.Message +} +"@ + + Set-Content -Path $tempScriptPath -Value $scriptContent + Write-Information "[VERBOSE] Created temporary script: $tempScriptPath" + + # Create and register the scheduled task + $taskName = "SetCertACL_$((Get-Date).Ticks)" + $action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$tempScriptPath`"" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2) + $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable + + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null + Write-Information "[VERBOSE] Scheduled task registered: $taskName" + + # Wait for task to complete + Write-Information "[VERBOSE] Waiting for scheduled task to complete..." + Start-Sleep -Seconds 5 + + # Check results + if (Test-Path "$env:TEMP\acl_success.txt") { + $successMessage = Get-Content "$env:TEMP\acl_success.txt" -Raw + Write-Information "ACL updated successfully using scheduled task method ($successMessage)" + Remove-Item "$env:TEMP\acl_success.txt" -Force -ErrorAction SilentlyContinue + $aclSet = $true + $aclMethod = "Scheduled Task" + } + elseif (Test-Path "$env:TEMP\acl_error.txt") { + $errorMessage = Get-Content "$env:TEMP\acl_error.txt" -Raw + Remove-Item "$env:TEMP\acl_error.txt" -Force -ErrorAction SilentlyContinue + throw "Scheduled task failed: $errorMessage" + } + else { + throw "Scheduled task did not complete or produce expected output" + } + + # Cleanup + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item $tempScriptPath -Force -ErrorAction SilentlyContinue + } + catch { + Write-Warning "Scheduled task method failed: $_" + } + } + + # Final check + if (-not $aclSet) { + throw "Failed to set ACL permissions using all available methods (Set-Acl, icacls, Scheduled Task)" + } + + Write-Information "ACL permissions configured successfully using: $aclMethod" + + } + catch { + Write-Error "Failed to update ACL on the private key: $_" + Write-Error "SQL Server may not be able to use this certificate without proper permissions." + throw $_ + } + + # ============================================================ + # STEP 6: Restart SQL Server Service (Optional) + # ============================================================ + if ($RestartService) { + Write-Information "Restarting SQL Server service..." + + try { + # Get current service status + $service = Get-Service -Name $serviceName -ErrorAction Stop + $originalStatus = $service.Status + + Write-Information "[VERBOSE] Current service status: $originalStatus" + + # Stop the service if running + if ($originalStatus -eq 'Running') { + Write-Information "Stopping SQL Server service: $serviceName" + Stop-Service -Name $serviceName -Force -ErrorAction Stop + + # Wait for service to stop (with timeout) + $stopTimeout = 60 + $elapsed = 0 + + while ((Get-Service -Name $serviceName).Status -ne 'Stopped' -and $elapsed -lt $stopTimeout) { + Start-Sleep -Seconds 2 + $elapsed += 2 + Write-Information "[VERBOSE] Waiting for service to stop... ($elapsed seconds)" + } + + if ((Get-Service -Name $serviceName).Status -ne 'Stopped') { + throw "Service did not stop within $stopTimeout seconds" + } + + Write-Information "SQL Server service stopped successfully" + } + + # Start the service + Write-Information "Starting SQL Server service: $serviceName" + Start-Service -Name $serviceName -ErrorAction Stop + + # Wait for service to start (with timeout) + $startTimeout = 90 + $elapsed = 0 + + while ((Get-Service -Name $serviceName).Status -ne 'Running' -and $elapsed -lt $startTimeout) { + Start-Sleep -Seconds 2 + $elapsed += 2 + Write-Information "[VERBOSE] Waiting for service to start... ($elapsed seconds)" + } + + $finalStatus = (Get-Service -Name $serviceName).Status + + if ($finalStatus -eq 'Running') { + Write-Information "SQL Server service restarted successfully" + } else { + throw "Service did not start within $startTimeout seconds. Current status: $finalStatus" + } + } + catch { + Write-Error "Failed to restart SQL Server service: $_" + Write-Warning "Certificate binding completed but service restart failed." + Write-Warning "Please restart SQL Server manually to apply the certificate binding." + Write-Warning "You can restart using: Restart-Service -Name '$serviceName' -Force" + + # Don't throw here - the certificate binding succeeded + # Just warn the user to restart manually + } + } else { + Write-Information "Service restart skipped. You must restart SQL Server for the certificate binding to take effect." + Write-Information "To restart: Restart-Service -Name '$serviceName' -Force" + } + + # ============================================================ + # SUCCESS + # ============================================================ + Write-Information "==========================================" + Write-Information "Certificate binding completed successfully!" + Write-Information "Instance: $InstanceName" + Write-Information "Certificate: $NewThumbprint" + Write-Information "Service Account: $SqlServiceUser" + Write-Information "Key Storage: $keyStorageType" + Write-Information "ACL Method: $aclMethod" + + if ($RestartService) { + Write-Information "Service Status: Restarted" + } else { + Write-Information "Service Status: Restart Required" + } + Write-Information "==========================================" + + return $true + } + catch { + Write-Error "Certificate binding failed for instance $InstanceName" + Write-Error "Error: $_" + Write-Information "[VERBOSE] Stack trace: $($_.ScriptStackTrace)" + return $false + } +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Get-KeyfactorSQLInventory.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Get-KeyfactorSQLInventory.ps1 new file mode 100644 index 00000000..c04a4db7 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Get-KeyfactorSQLInventory.ps1 @@ -0,0 +1,70 @@ +function GET-KeyfactorSQLInventory { + # Retrieve all SQL Server instances + $sqlInstances = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server").InstalledInstances + Write-Information "There are $($sqlInstances.Count) instances that will be checked for certificates." + + # Dictionary to store instance names by thumbprint + $commonInstances = @{} + + # First loop: gather thumbprints for each instance + foreach ($instance in $sqlInstances) { + Write-Information "Checking instance: $instance for Certificates." + + # Get the registry path for the SQL instance + $fullInstanceName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance + $regLocation = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$fullInstanceName\MSSQLServer\SuperSocketNetLib" + + try { + # Retrieve the certificate thumbprint from the registry + $thumbprint = (Get-ItemPropertyValue -Path $regLocation -Name "Certificate" -ErrorAction Stop).ToUpper() + + if ($thumbprint) { + # Store instance names by thumbprint + if ($commonInstances.ContainsKey($thumbprint)) { + $commonInstances[$thumbprint] += ",$instance" + } else { + $commonInstances[$thumbprint] = $instance + } + } + } catch { + Write-Information "No certificate found for instance: $instance." + } + } + + # Array to store results + $myBoundCerts = @() + + # Second loop: process each unique thumbprint and gather certificate data + foreach ($kp in $commonInstances.GetEnumerator()) { + $thumbprint = $kp.Key + $instanceNames = $kp.Value + + # Find the certificate in the local machine store + $certStore = "My" + $cert = Get-ChildItem -Path "Cert:\LocalMachine\$certStore\$thumbprint" -ErrorAction SilentlyContinue + + if ($cert) { + # Create a hashtable with the certificate parameters + $sqlSettingsDict = @{ + InstanceName = $instanceNames + ProviderName = $cert.PrivateKey.CspKeyContainerInfo.ProviderName + } + + # Build the inventory item for this certificate + $inventoryItem = [PSCustomObject]@{ + Certificates = [Convert]::ToBase64String($cert.RawData) + Alias = $thumbprint + PrivateKeyEntry = $cert.HasPrivateKey + UseChainLevel = $false + ItemStatus = "Unknown" # OrchestratorInventoryItemStatus.Unknown equivalent + Parameters = $sqlSettingsDict + } + + # Add the inventory item to the results array + $myBoundCerts += $inventoryItem + } + } + + # Return the array of inventory items + return $myBoundCerts | ConvertTo-Json +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/New-KeyfactorSQLBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/New-KeyfactorSQLBinding.ps1 new file mode 100644 index 00000000..5d3f3339 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/New-KeyfactorSQLBinding.ps1 @@ -0,0 +1,60 @@ +function New-KeyfactorSQLBinding { + param ( + [string]$SQLInstance, + [string]$RenewalThumbprint, + [string]$NewThumbprint, + [switch]$RestartService = $false + ) + + function Get-SqlCertRegistryLocation($InstanceName) { + return "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$InstanceName\MSSQLServer\SuperSocketNetLib" + } + + $bindingSuccess = $true # Track success/failure + + try { + $SQLInstances = $SQLInstance -split ',' | ForEach-Object { $_.Trim() } + + foreach ($instance in $SQLInstances) { + try { + $fullInstance = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance -ErrorAction Stop + $regLocation = Get-SqlCertRegistryLocation -InstanceName $fullInstance + + if (-not (Test-Path $regLocation)) { + Write-Error "Registry location not found: $regLocation" + $bindingSuccess = $false + continue + } + Write-Information "[VERBOSE] Instance: $instance" + Write-Information "[VERBOSE] Full Instance: $fullInstance" + Write-Information "[VERBOSE] Registry Location: $regLocation" + Write-Information "[VERBOSE] Current Thumbprint: $currentThumbprint" + + $currentThumbprint = Get-ItemPropertyValue -Path $regLocation -Name "Certificate" -ErrorAction SilentlyContinue + + if ($RenewalThumbprint -and $RenewalThumbprint -contains $currentThumbprint) { + Write-Information "Renewal thumbprint matches for instance: $fullInstance" + $result = Set-KeyfactorSQLCertificateBinding -InstanceName $instance -NewThumbprint $NewThumbprint -RestartService:$RestartService + } elseif (-not $RenewalThumbprint) { + Write-Information "No renewal thumbprint provided. Binding certificate to instance: $fullInstance" + $result = Set-KeyfactorSQLCertificateBinding -InstanceName $instance -NewThumbprint $NewThumbprint -RestartService:$RestartService + } + + if (-not $result) { + Write-Error "Failed to bind certificate for instance: $instance" + $bindingSuccess = $false + } + } + catch { + Write-Error "Error processing instance '$instance': $($_.Exception.Message)" + $bindingSuccess = $false + } + } + } + catch { + Write-Error "An unexpected error occurred: $($_.Exception.Message)" + return $false + } + + return $bindingSuccess +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Remove-KeyfactorSQLCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Remove-KeyfactorSQLCertificate.ps1 new file mode 100644 index 00000000..329c2af4 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Public/Remove-KeyfactorSQLCertificate.ps1 @@ -0,0 +1,56 @@ +function Remove-KeyfactorSQLCertificate { + param ( + [string]$SQLInstanceNames, # Comma-separated list of SQL instances + [switch]$RestartService # Restart SQL Server after unbinding + ) + + $unBindingSuccess = $true # Track success/failure + + try { + + $instances = $SQLInstanceNames -split ',' | ForEach-Object { $_.Trim() } + + foreach ($instance in $instances) { + try { + # Resolve full instance name from registry + $fullInstance = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance + $regPath = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$fullInstance\MSSQLServer\SuperSocketNetLib" + + # Get current thumbprint + $certificateThumbprint = Get-ItemPropertyValue -Path $regPath -Name "Certificate" -ErrorAction SilentlyContinue + + if ($certificateThumbprint) { + Write-Information "Unbinding certificate from SQL Server instance: $instance (Thumbprint: $certificateThumbprint)" + + # Instead of deleting, set to an empty string to prevent SQL startup issues + Set-ItemProperty -Path $regPath -Name "Certificate" -Value "" + + Write-Information "Successfully unbound certificate from SQL Server instance: $instance." + } else { + Write-Warning "No certificate bound to SQL Server instance: $instance." + } + + # Restart service if required + if ($RestartService) { + $serviceName = Get-KeyfactorSQLServiceName -InstanceName $instance + Write-Information "Restarting SQL Server service: $serviceName..." + Restart-Service -Name $serviceName -Force + Write-Information "SQL Server service restarted successfully." + } + } + catch { + Write-Error "Failed to unbind certificate from instance: $instance. Error: $_" + $unBindingSuccess = $false + } + } + } + catch { + Write-Error "An unexpected error occurred: $($_.Exception.Message)" + return $false + } + + return $unBindingSuccess + +# Example usage: +# Bind-CertificateToSqlInstance -Thumbprint "123ABC456DEF" -SqlInstanceName "MSSQLSERVER" +} \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/RoleCapabilities/Keyfactor.WinCert.SQL.psrc b/IISU/PowerShell/Keyfactor.WinCert.SQL/RoleCapabilities/Keyfactor.WinCert.SQL.psrc new file mode 100644 index 00000000..672090e0 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/RoleCapabilities/Keyfactor.WinCert.SQL.psrc @@ -0,0 +1,61 @@ +# +# Keyfactor.WinCert.SQL.psrc +# JEA Role Capability file for Keyfactor Windows Certificate Store (SQL Server operations) +# +# INSTALL LOCATION: +# This file must live inside the module folder that PowerShell can discover: +# C:\Program Files\WindowsPowerShell\Modules\ +# Keyfactor.WinCert.SQL\ +# RoleCapabilities\ +# Keyfactor.WinCert.SQL.psrc <-- this file +# +# PowerShell finds the role capability by name ('Keyfactor.WinCert.SQL') by searching +# all modules in $env:PSModulePath for a RoleCapabilities subfolder with a matching .psrc. +# +# LANGUAGE MODE NOTE: +# The session configuration (KeyfactorWinCert.pssc) sets LanguageMode = 'ConstrainedLanguage'. +# Module code loaded from a trusted path ($env:PSModulePath) runs as FULLY TRUSTED and can +# use .NET APIs freely. The ConstrainedLanguage restriction only applies to caller scriptblocks +# sent by the orchestrator (e.g., param() blocks for parameter passing). +# +@{ + GUID = 'a7b93d2f-5e4c-4b1a-8f6e-2c9d0e3f7a1b' + Author = 'Keyfactor' + Description = 'Role capabilities for Keyfactor Windows Certificate Store SQL Server operations (inventory, bind, unbind)' + + # Import both modules so their functions are pre-loaded and run as trusted code. + # Both modules MUST be installed under C:\Program Files\WindowsPowerShell\Modules\ so that + # PowerShell treats them as trusted (required for .NET API usage in ConstrainedLanguage sessions). + ModulesToImport = @('Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL') + + # Functions the orchestrator is allowed to invoke in this JEA session. + VisibleFunctions = @( + 'Get-KeyfactorSQLInventory', + 'New-KeyfactorSQLBinding', + 'Remove-KeyfactorSQLCertificate' + ) + + # Cmdlets available to caller scriptblocks (param() wrappers sent by PSHelper). + # Module functions are trusted and can use any cmdlet internally regardless of this list. + VisibleCmdlets = @( + 'ConvertTo-Json', + 'ConvertFrom-Json', + 'Write-Output', + 'Write-Verbose', + 'Write-Warning', + 'Write-Error', + 'Write-Information' + ) + + # icacls.exe is called via the PowerShell pipeline by Set-KeyfactorSQLCertificateBinding. + # Full path is required. + VisibleExternalCommands = @( + 'C:\Windows\System32\icacls.exe' + ) + + # No variables need to be pre-defined for the orchestrator. + VisibleVariables = @() + + # No aliases beyond what the module exports. + VisibleAliases = @() +} diff --git a/IISU/PowerShell/WinADFSScripts.ps1 b/IISU/PowerShell/WinADFSScripts.ps1 index 7f6ce516..d9917117 100644 --- a/IISU/PowerShell/WinADFSScripts.ps1 +++ b/IISU/PowerShell/WinADFSScripts.ps1 @@ -531,13 +531,13 @@ function Grant-AdfsCertificatePermissions { # If no service account provided, try to get it if ([string]::IsNullOrWhiteSpace($ServiceAccountName)) { - Write-Verbose " Service account not provided, attempting to detect..." + Write-Information "[VERBOSE] Service account not provided, attempting to detect..." try { # Try to get from ADFS properties $adfsProps = Get-ADFSProperties -ErrorAction Stop $ServiceAccountName = $adfsProps.ServiceAccountName - Write-Verbose " Detected service account: $ServiceAccountName" + Write-Information "[VERBOSE] Detected service account: $ServiceAccountName" } catch { Write-Warning "Could not detect ADFS service account" @@ -548,7 +548,7 @@ function Grant-AdfsCertificatePermissions { try { $service = Get-WmiObject Win32_Service -Filter "Name='adfssrv'" -ErrorAction Stop $ServiceAccountName = $service.StartName - Write-Verbose " Detected from service: $ServiceAccountName" + Write-Information "[VERBOSE] Detected from service: $ServiceAccountName" } catch { Write-Warning "Could not detect service account from Windows service" @@ -572,7 +572,7 @@ function Grant-AdfsCertificatePermissions { # Check if service account is a built-in account (which doesn't need explicit permissions) $builtInAccounts = @('NT AUTHORITY\SYSTEM', 'NT AUTHORITY\NETWORK SERVICE', 'LocalSystem', 'SYSTEM') if ($builtInAccounts -contains $ServiceAccountName) { - Write-Verbose " Service runs as built-in account ($ServiceAccountName) - explicit permissions not needed" + Write-Information "[VERBOSE] Service runs as built-in account ($ServiceAccountName) - explicit permissions not needed" return [PSCustomObject]@{ Success = $true @@ -583,7 +583,7 @@ function Grant-AdfsCertificatePermissions { } } - Write-Verbose " Granting permissions to: $ServiceAccountName" + Write-Information "[VERBOSE] Granting permissions to: $ServiceAccountName" # Get the private key $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) @@ -599,7 +599,7 @@ function Grant-AdfsCertificatePermissions { # Check if account already has permissions $existingRule = $acl.Access | Where-Object { $_.IdentityReference -eq $ServiceAccountName } if ($existingRule) { - Write-Verbose " ✓ Service account already has permissions" + Write-Information "[VERBOSE] ✓ Service account already has permissions" return [PSCustomObject]@{ Success = $true AlreadyGranted = $true @@ -621,7 +621,7 @@ function Grant-AdfsCertificatePermissions { # Set the ACL Set-Acl -Path $privateKeyPath -AclObject $acl - Write-Verbose " ✓ Permissions granted to $ServiceAccountName" + Write-Information "[VERBOSE] ✓ Permissions granted to $ServiceAccountName" return [PSCustomObject]@{ Success = $true @@ -662,7 +662,7 @@ function Grant-AdfsCertificatePermissionsOLD { ) try { - Write-Verbose "Granting permissions to service account on $env:COMPUTERNAME..." + Write-Information "[VERBOSE] Granting permissions to service account on $env:COMPUTERNAME..." # Get the certificate $cert = Get-ChildItem -Path "Cert:\LocalMachine\My\$CertificateThumbprint" -ErrorAction Stop diff --git a/IISU/PowerShell/WinCertScripts.ps1 b/IISU/PowerShell/WinCertScripts.ps1 index f03a3c1d..0912c6f9 100644 --- a/IISU/PowerShell/WinCertScripts.ps1 +++ b/IISU/PowerShell/WinCertScripts.ps1 @@ -171,7 +171,7 @@ function Add-KFCertificateToStore{ try { Write-Information "Entering PowerShell Script Add-KFCertificate" - Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" + Write-Information "[VERBOSE] Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" # Get the thumbprint of the passed in certificate # Convert password to secure string if provided, otherwise use $null @@ -210,13 +210,13 @@ function Add-KFCertificateToStore{ $arguments = @('-f') if ($PrivateKeyPassword) { - Write-Verbose "Has a private key" + Write-Information "[VERBOSE] Has a private key" $arguments += '-p' $arguments += $PrivateKeyPassword } if ($CryptoServiceProvider) { - Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider" + Write-Information "[VERBOSE] Has a CryptoServiceProvider: $CryptoServiceProvider" $arguments += '-csp' $arguments += $CryptoServiceProvider } @@ -230,7 +230,7 @@ function Add-KFCertificateToStore{ if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } }) -join ' ' - write-Verbose "Running certutil with arguments: $argLine" + Write-Information "[VERBOSE] Running certutil with arguments: $argLine" # Setup process execution $processInfo = New-Object System.Diagnostics.ProcessStartInfo @@ -441,7 +441,7 @@ function New-KFIISSiteBinding { ) Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue - Write-Verbose "Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" + Write-Information "[VERBOSE] Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" try { # Step 1: Perform verifications and get management info @@ -458,7 +458,7 @@ function New-KFIISSiteBinding { # Step 2: Remove existing HTTPS bindings for this binding info $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Verbose "Removing existing HTTPS bindings for: $searchBindings" + Write-Information "[VERBOSE] Removing existing HTTPS bindings for: $searchBindings" $removalResult = Remove-ExistingIISBinding -SiteName $SiteName -BindingInfo $searchBindings -UseIISDrive $managementInfo.UseIISDrive if ($removalResult.Status -eq 'Error') { @@ -467,14 +467,14 @@ function New-KFIISSiteBinding { # Step 3: Determine SslFlags supported by Microsoft.Web.Administration if ($SslFlags -gt 3) { - Write-Verbose "SslFlags value $SslFlags exceeds managed API range (0–3). Applying reduced flags for creation." + Write-Information "[VERBOSE] SslFlags value $SslFlags exceeds managed API range (0–3). Applying reduced flags for creation." $SslFlagsApplied = ($SslFlags -band 3) } else { $SslFlagsApplied = $SslFlags } # Step 4: Add the new binding with the reduced flag set - Write-Verbose "Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" + Write-Information "[VERBOSE] Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" $addParams = @{ SiteName = $SiteName @@ -496,7 +496,7 @@ function New-KFIISSiteBinding { # Step 5: If extended flags, update via appcmd.exe if ($SslFlags -gt 3) { - Write-Verbose "Applying full SslFlags=$SslFlags via appcmd" + Write-Information "[VERBOSE] Applying full SslFlags=$SslFlags via appcmd" $appcmd = Join-Path $env:windir "System32\inetsrv\appcmd.exe" @@ -514,16 +514,16 @@ function New-KFIISSiteBinding { # Build binding argument for appcmd $bindingArg = "/bindings.[protocol='https',bindingInformation='$bindingInfo'].sslFlags:$SslFlags" - Write-Verbose "Running appcmd: $appcmd $siteArg $bindingArg" + Write-Information "[VERBOSE] Running appcmd: $appcmd $siteArg $bindingArg" $appcmdOutput = & $appcmd set site $siteArg $bindingArg 2>&1 - Write-Verbose "appcmd output: $appcmdOutput" + Write-Information "[VERBOSE] appcmd output: $appcmdOutput" #& $appcmd set site $siteArg $bindingArg | Out-Null if ($LASTEXITCODE -ne 0) { Write-Warning "appcmd failed to set extended SslFlags ($SslFlags) for binding $bindingInfo." } else { - Write-Verbose "Successfully updated SslFlags to $SslFlags via appcmd." + Write-Information "[VERBOSE] Successfully updated SslFlags to $SslFlags via appcmd." } } @@ -552,11 +552,11 @@ function Remove-ExistingIISBinding { [bool]$UseIISDrive ) - Write-Verbose "Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" + Write-Information "[VERBOSE] Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" try { if ($UseIISDrive) { - Write-Verbose "Using IIS Drive to remove binding" + Write-Information "[VERBOSE] Using IIS Drive to remove binding" $sitePath = "IIS:\Sites\$SiteName" $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { @@ -567,13 +567,13 @@ function Remove-ExistingIISBinding { $bindingInfo = $binding.GetAttributeValue("bindingInformation") $protocol = $binding.protocol - Write-Verbose "Removing binding: $bindingInfo ($protocol)" + Write-Information "[VERBOSE] Removing binding: $bindingInfo ($protocol)" Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false - Write-Verbose "Successfully removed binding" + Write-Information "[VERBOSE] Successfully removed binding" } } else { - Write-Verbose "Using Web Administration assembly to remove binding" + Write-Information "[VERBOSE] Using Web Administration assembly to remove binding" # ServerManager fallback Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" $iis = New-Object Microsoft.Web.Administration.ServerManager @@ -584,13 +584,13 @@ function Remove-ExistingIISBinding { } foreach ($binding in $httpsBindings) { - Write-Verbose "Removing binding: $($binding.BindingInformation)" + Write-Information "[VERBOSE] Removing binding: $($binding.BindingInformation)" $site.Bindings.Remove($binding) - Write-Verbose "Successfully removed binding" + Write-Information "[VERBOSE] Successfully removed binding" } $iis.CommitChanges() - Write-Verbose "Committed changes to IIS" + Write-Information "[VERBOSE] Committed changes to IIS" } return New-ResultObject -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" @@ -633,7 +633,7 @@ function Add-IISBindingWithSSL { [bool]$UseIISDrive ) - Write-Verbose "Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" + Write-Information "[VERBOSE] Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" try { if ($UseIISDrive) { @@ -651,22 +651,22 @@ function Add-IISBindingWithSSL { $bindingParams.HostHeader = $Hostname } - Write-Verbose "Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" + Write-Information "[VERBOSE] Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" New-WebBinding @bindingParams # Bind SSL certificate if HTTPS if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Verbose "Searching for binding: $searchBindings" + Write-Information "[VERBOSE] Searching for binding: $searchBindings" $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { $_.bindingInformation -eq $searchBindings } if ($binding) { - Write-Verbose "Binding SSL certificate with thumbprint: $Thumbprint" + Write-Information "[VERBOSE] Binding SSL certificate with thumbprint: $Thumbprint" $null = $binding.AddSslCertificate($Thumbprint, $StoreName) - Write-Verbose "SSL certificate successfully bound" + Write-Information "[VERBOSE] SSL certificate successfully bound" return New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and SSL certificate successfully applied" } else { return New-ResultObject -Status Error -Code 202 -Step BindSSL -ErrorMessage "No binding found for: $searchBindings" @@ -718,7 +718,7 @@ function Remove-KFIISSiteBinding { [Parameter(Mandatory = $false)] [string] $Hostname ) - Write-Verbose "Entering PowerShell Scrip Remove-KFIISiteBinding with arguments Sitename: '$SiteName', IP Address: '$IPAddress', Port: $Port, Hostname: '$Hostname'" + Write-Information "[VERBOSE] Entering PowerShell Scrip Remove-KFIISiteBinding with arguments Sitename: '$SiteName', IP Address: '$IPAddress', Port: $Port, Hostname: '$Hostname'" try { Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" @@ -737,14 +737,14 @@ function Remove-KFIISSiteBinding { $searchBindingInfo = if ($HostName) { "$IPAddress`:$Port`:$HostName" } else { "$IPAddress`:$Port`:" } - Write-Verbose "Searching Site for bindings: $searchBindingInfo" + Write-Information "[VERBOSE] Searching Site for bindings: $searchBindingInfo" $httpsBinding = $site.Bindings | Where-Object { $_.bindingInformation -eq $searchBindingInfo -and $_.protocol -eq 'https' } if ($httpsBinding) { $site.Bindings.Remove($httpsBinding) $serverManager.CommitChanges() - Write-Verbose "Removed binding $httpsBinding from site '$SiteName'." + Write-Information "[VERBOSE] Removed binding $httpsBinding from site '$SiteName'." return $true } @@ -823,6 +823,7 @@ function Remove-KFIISCertificateIfUnused { ##### # Function to get certificate information for a SQL Server instance +# Migrated function GET-KFSQLInventory { # Retrieve all SQL Server instances $sqlInstances = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server").InstalledInstances @@ -894,6 +895,7 @@ function GET-KFSQLInventory { return $myBoundCerts | ConvertTo-Json } +# Migrated to New-KeyfactorSQLCertificate function Bind-KFSqlCertificate { param ( [string]$SQLInstance, @@ -921,10 +923,10 @@ function Bind-KFSqlCertificate { $bindingSuccess = $false continue } - Write-Verbose "Instance: $instance" - Write-Verbose "Full Instance: $fullInstance" - Write-Verbose "Registry Location: $regLocation" - Write-Verbose "Current Thumbprint: $currentThumbprint" + Write-Information "[VERBOSE] Instance: $instance" + Write-Information "[VERBOSE] Full Instance: $fullInstance" + Write-Information "[VERBOSE] Registry Location: $regLocation" + Write-Information "[VERBOSE] Current Thumbprint: $currentThumbprint" $currentThumbprint = Get-ItemPropertyValue -Path $regLocation -Name "Certificate" -ErrorAction SilentlyContinue @@ -955,6 +957,7 @@ function Bind-KFSqlCertificate { return $bindingSuccess } +# Migrated function Set-KFSQLCertificateBinding { <# .SYNOPSIS @@ -1003,8 +1006,8 @@ function Set-KFSQLCertificateBinding { $fullInstance = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $InstanceName -ErrorAction Stop $RegistryPath = Get-SqlCertRegistryLocation -InstanceName $fullInstance - Write-Verbose "Full instance name: $fullInstance" - Write-Verbose "Registry path: $RegistryPath" + Write-Information "[VERBOSE] Full instance name: $fullInstance" + Write-Information "[VERBOSE] Registry path: $RegistryPath" Write-Information "SQL Server instance registry path located: $RegistryPath" } catch { @@ -1022,9 +1025,9 @@ function Set-KFSQLCertificateBinding { $currentThumbprint = Get-ItemPropertyValue -Path $RegistryPath -Name "Certificate" -ErrorAction SilentlyContinue if ($currentThumbprint) { - Write-Verbose "Current certificate thumbprint: $currentThumbprint" + Write-Information "[VERBOSE] Current certificate thumbprint: $currentThumbprint" } else { - Write-Verbose "No existing certificate thumbprint found" + Write-Information "[VERBOSE] No existing certificate thumbprint found" } # Set new thumbprint @@ -1061,19 +1064,19 @@ function Set-KFSQLCertificateBinding { # Normalize service account name for ACL operations if ($SqlServiceUser -eq "LocalSystem") { $SqlServiceUser = "NT AUTHORITY\SYSTEM" - Write-Verbose "Normalized LocalSystem to: $SqlServiceUser" + Write-Information "[VERBOSE] Normalized LocalSystem to: $SqlServiceUser" } elseif ($SqlServiceUser -match "^NT Service\\") { # NT Service accounts are already in correct format - Write-Verbose "Using NT Service account: $SqlServiceUser" + Write-Information "[VERBOSE] Using NT Service account: $SqlServiceUser" } elseif ($SqlServiceUser.StartsWith(".\")) { # Local account - convert to machine\user format $SqlServiceUser = "$env:COMPUTERNAME$($SqlServiceUser.Substring(1))" - Write-Verbose "Normalized local account to: $SqlServiceUser" + Write-Information "[VERBOSE] Normalized local account to: $SqlServiceUser" } - Write-Verbose "Service name: $serviceName" + Write-Information "[VERBOSE] Service name: $serviceName" Write-Information "SQL Server service account: $SqlServiceUser" } catch { @@ -1094,7 +1097,7 @@ function Set-KFSQLCertificateBinding { throw "Certificate with thumbprint $NewThumbprint not found in LocalMachine\My store" } - Write-Verbose "Certificate found: $($Cert.Subject)" + Write-Information "[VERBOSE] Certificate found: $($Cert.Subject)" if (-not $Cert.HasPrivateKey) { throw "Certificate does not have a private key" @@ -1112,8 +1115,8 @@ function Set-KFSQLCertificateBinding { if ($rsaKey -is [System.Security.Cryptography.RSACng]) { $privKey = $rsaKey.Key.UniqueName $keyStorageType = "CNG" - Write-Verbose "Certificate uses CNG key storage" - Write-Verbose "CNG key unique name: $privKey" + Write-Information "[VERBOSE] Certificate uses CNG key storage" + Write-Information "[VERBOSE] CNG key unique name: $privKey" # CNG keys can be in multiple locations - check them all $possiblePaths = @( @@ -1122,19 +1125,19 @@ function Set-KFSQLCertificateBinding { "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\$privKey" ) - Write-Verbose "Searching for CNG private key in known locations..." + Write-Information "[VERBOSE] Searching for CNG private key in known locations..." foreach ($path in $possiblePaths) { - Write-Verbose "Checking: $path" + Write-Information "[VERBOSE] Checking: $path" if (Test-Path $path) { $privKeyPath = Get-Item $path -ErrorAction Stop - Write-Verbose "Found CNG private key at: $path" + Write-Information "[VERBOSE] Found CNG private key at: $path" break } } # If not found in standard locations, search more broadly if (-not $privKeyPath) { - Write-Verbose "Key not found in standard locations. Searching all Crypto directories..." + Write-Information "[VERBOSE] Key not found in standard locations. Searching all Crypto directories..." $searchPaths = @( "$($env:ProgramData)\Microsoft\Crypto\Keys", @@ -1147,7 +1150,7 @@ function Set-KFSQLCertificateBinding { $found = Get-ChildItem -Path $searchPath -Filter "*$privKey*" -ErrorAction SilentlyContinue | Select-Object -First 1 if ($found) { $privKeyPath = $found - Write-Verbose "Found CNG private key at: $($privKeyPath.FullName)" + Write-Information "[VERBOSE] Found CNG private key at: $($privKeyPath.FullName)" break } } @@ -1160,13 +1163,13 @@ function Set-KFSQLCertificateBinding { } } catch { - Write-Verbose "CNG key detection failed or not applicable: $_" - Write-Verbose "Exception type: $($_.Exception.GetType().FullName)" + Write-Information "[VERBOSE] CNG key detection failed or not applicable: $_" + Write-Information "[VERBOSE] Exception type: $($_.Exception.GetType().FullName)" } # Fallback to CAPI/CSP (legacy certificates) if (-not $privKey -or -not $privKeyPath) { - Write-Verbose "Attempting CAPI/CSP key detection..." + Write-Information "[VERBOSE] Attempting CAPI/CSP key detection..." try { if ($Cert.PrivateKey -and $Cert.PrivateKey.CspKeyContainerInfo) { @@ -1174,15 +1177,15 @@ function Set-KFSQLCertificateBinding { $keyPath = "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\" $keyStorageType = "CAPI/CSP" - Write-Verbose "CAPI/CSP key unique name: $privKey" - Write-Verbose "Expected path: $keyPath$privKey" + Write-Information "[VERBOSE] CAPI/CSP key unique name: $privKey" + Write-Information "[VERBOSE] Expected path: $keyPath$privKey" $privKeyPath = Get-Item "$keyPath\$privKey" -ErrorAction Stop Write-Information "Certificate uses CAPI/CSP (legacy) key storage" } } catch { - Write-Verbose "CAPI/CSP key detection failed: $_" + Write-Information "[VERBOSE] CAPI/CSP key detection failed: $_" } } @@ -1202,7 +1205,7 @@ function Set-KFSQLCertificateBinding { foreach ($searchPath in $allCryptoPaths) { if (Test-Path $searchPath) { - Write-Verbose "Searching in: $searchPath" + Write-Information "[VERBOSE] Searching in: $searchPath" $found = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*$privKey*" -or $_.Name -eq $privKey } | Select-Object -First 1 @@ -1221,13 +1224,13 @@ function Set-KFSQLCertificateBinding { } Write-Information "Private key located at: $($privKeyPath.FullName)" - Write-Verbose "Key storage type: $keyStorageType" - Write-Verbose "Key file size: $($privKeyPath.Length) bytes" + Write-Information "[VERBOSE] Key storage type: $keyStorageType" + Write-Information "[VERBOSE] Key file size: $($privKeyPath.Length) bytes" # Verify we can read the key file try { $acl = Get-Acl -Path $privKeyPath.FullName -ErrorAction Stop - Write-Verbose "Successfully accessed private key file ACL" + Write-Information "[VERBOSE] Successfully accessed private key file ACL" } catch { Write-Warning "Could not read ACL from private key file: $_" @@ -1249,7 +1252,7 @@ function Set-KFSQLCertificateBinding { # Attempt 1: Try Set-Acl (works in most local scenarios and some SSH sessions) try { - Write-Verbose "Attempting ACL update using Set-Acl method..." + Write-Information "[VERBOSE] Attempting ACL update using Set-Acl method..." $Acl = Get-Acl -Path $privKeyPath -ErrorAction Stop $Ar = New-Object System.Security.AccessControl.FileSystemAccessRule( @@ -1278,19 +1281,19 @@ function Set-KFSQLCertificateBinding { } catch { Write-Warning "Set-Acl method failed: $_" - Write-Verbose "Error details: $($_.Exception.Message)" + Write-Information "[VERBOSE] Error details: $($_.Exception.Message)" } # Attempt 2: Use icacls (more reliable in SSH sessions) if (-not $aclSet) { - Write-Verbose "Attempting ACL update using icacls method..." + Write-Information "[VERBOSE] Attempting ACL update using icacls method..." try { # Execute icacls to grant Read permissions $icaclsResult = & icacls.exe $privKeyPath.FullName /grant "${SqlServiceUser}:(R)" 2>&1 if ($LASTEXITCODE -eq 0) { - Write-Verbose "icacls command executed successfully" + Write-Information "[VERBOSE] icacls command executed successfully" # Verify with icacls $verifyResult = & icacls.exe $privKeyPath.FullName 2>&1 @@ -1304,7 +1307,7 @@ function Set-KFSQLCertificateBinding { } } else { Write-Warning "icacls failed with exit code $LASTEXITCODE" - Write-Verbose "icacls output: $icaclsResult" + Write-Information "[VERBOSE] icacls output: $icaclsResult" } } catch { @@ -1348,7 +1351,7 @@ try { "@ Set-Content -Path $tempScriptPath -Value $scriptContent - Write-Verbose "Created temporary script: $tempScriptPath" + Write-Information "[VERBOSE] Created temporary script: $tempScriptPath" # Create and register the scheduled task $taskName = "SetCertACL_$((Get-Date).Ticks)" @@ -1358,10 +1361,10 @@ try { $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null - Write-Verbose "Scheduled task registered: $taskName" + Write-Information "[VERBOSE] Scheduled task registered: $taskName" # Wait for task to complete - Write-Verbose "Waiting for scheduled task to complete..." + Write-Information "[VERBOSE] Waiting for scheduled task to complete..." Start-Sleep -Seconds 5 # Check results @@ -1415,7 +1418,7 @@ try { $service = Get-Service -Name $serviceName -ErrorAction Stop $originalStatus = $service.Status - Write-Verbose "Current service status: $originalStatus" + Write-Information "[VERBOSE] Current service status: $originalStatus" # Stop the service if running if ($originalStatus -eq 'Running') { @@ -1429,7 +1432,7 @@ try { while ((Get-Service -Name $serviceName).Status -ne 'Stopped' -and $elapsed -lt $stopTimeout) { Start-Sleep -Seconds 2 $elapsed += 2 - Write-Verbose "Waiting for service to stop... ($elapsed seconds)" + Write-Information "[VERBOSE] Waiting for service to stop... ($elapsed seconds)" } if ((Get-Service -Name $serviceName).Status -ne 'Stopped') { @@ -1450,7 +1453,7 @@ try { while ((Get-Service -Name $serviceName).Status -ne 'Running' -and $elapsed -lt $startTimeout) { Start-Sleep -Seconds 2 $elapsed += 2 - Write-Verbose "Waiting for service to start... ($elapsed seconds)" + Write-Information "[VERBOSE] Waiting for service to start... ($elapsed seconds)" } $finalStatus = (Get-Service -Name $serviceName).Status @@ -1498,11 +1501,12 @@ try { catch { Write-Error "Certificate binding failed for instance $InstanceName" Write-Error "Error: $_" - Write-Verbose "Stack trace: $($_.ScriptStackTrace)" + Write-Information "[VERBOSE] Stack trace: $($_.ScriptStackTrace)" return $false } } +# Migrated function Unbind-KFSqlCertificate { param ( [string]$SQLInstanceNames, # Comma-separated list of SQL instances @@ -1560,6 +1564,7 @@ function Unbind-KFSqlCertificate { # Bind-CertificateToSqlInstance -Thumbprint "123ABC456DEF" -SqlInstanceName "MSSQLSERVER" } +# Migrated function Get-SqlServiceName { param ( [string]$InstanceName @@ -1571,6 +1576,7 @@ function Get-SqlServiceName { } } +# Migrated function Get-SQLServiceUser { param ( [Parameter(Mandatory = $true)] @@ -1593,6 +1599,7 @@ function Get-SQLServiceUser { ##### ##### ReEnrollment (ODKG) functions +# Migrated function New-CSREnrollment { param ( [string]$SubjectText, @@ -1641,7 +1648,7 @@ KeySpec = 0 $sanContent "@ - Write-Verbose "INF Contents: $infContent" + Write-Information "[VERBOSE] INF Contents: $infContent" # Path to temporary INF file $infFile = [System.IO.Path]::GetTempFileName() + ".inf" @@ -1702,11 +1709,11 @@ function Import-SignedCertificate { try { # Step 1: Convert raw certificate data to Base64 string with line breaks - Write-Verbose "Converting raw certificate data to Base64 string." + Write-Information "[VERBOSE] Converting raw certificate data to Base64 string." $csrData = [System.Convert]::ToBase64String($RawData, [System.Base64FormattingOptions]::InsertLineBreaks) # Step 2: Create PEM-formatted certificate content - Write-Verbose "Creating PEM-formatted certificate content." + Write-Information "[VERBOSE] Creating PEM-formatted certificate content." $certContent = @( "-----BEGIN CERTIFICATE-----" $csrData @@ -1714,18 +1721,18 @@ function Import-SignedCertificate { ) -join "`n" # Step 3: Create a temporary file for the certificate - Write-Verbose "Creating a temporary file for the certificate." + Write-Information "[VERBOSE] Creating a temporary file for the certificate." $cerFilename = [System.IO.Path]::GetTempFileName() Set-Content -Path $cerFilename -Value $certContent -Force - Write-Verbose "Temporary certificate file created at: $cerFilename" + Write-Information "[VERBOSE] Temporary certificate file created at: $cerFilename" # Step 4: Import the certificate into the specified store - Write-Verbose "Importing the certificate to the store: Cert:\LocalMachine\$StoreName" + Write-Information "[VERBOSE] Importing the certificate to the store: Cert:\LocalMachine\$StoreName" Set-Location -Path "Cert:\LocalMachine\$StoreName" $importResult = Import-Certificate -FilePath $cerFilename if ($importResult) { - Write-Verbose "Certificate successfully imported to Cert:\LocalMachine\$StoreName." + Write-Information "[VERBOSE] Certificate successfully imported to Cert:\LocalMachine\$StoreName." } else { throw "Certificate import failed." } @@ -1733,7 +1740,7 @@ function Import-SignedCertificate { # Step 5: Cleanup temporary file if (Test-Path $cerFilename) { Remove-Item -Path $cerFilename -Force - Write-Verbose "Temporary file deleted: $cerFilename" + Write-Information "[VERBOSE] Temporary file deleted: $cerFilename" } # Step 6: Return the imported certificate's thumbprint @@ -1793,7 +1800,7 @@ function Get-CertificateCSP { } } catch { - Write-Verbose "CNG provider lookup failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] CNG provider lookup failed: $($_.Exception.Message)" } return $null } @@ -1821,7 +1828,7 @@ function Get-CertificateCSP { } } catch { - Write-Verbose "RSA CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] RSA CNG detection failed: $($_.Exception.Message)" } # ── 3. ECC / ECDsa (ECDsaCng) ───────────────────────────────────────── @@ -1832,12 +1839,12 @@ function Get-CertificateCSP { $providerName = Get-CngProviderName $ecKey if ($providerName) { return $providerName } - Write-Verbose "ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" + Write-Information "[VERBOSE] ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" return "" } } catch { - Write-Verbose "ECDsa CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] ECDsa CNG detection failed: $($_.Exception.Message)" } # ── 4. DSA (bonus) ──────────────────────────────────────────────────── @@ -1847,15 +1854,15 @@ function Get-CertificateCSP { $providerName = Get-CngProviderName $dsaKey if ($providerName) { return $providerName } - Write-Verbose "DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" + Write-Information "[VERBOSE] DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" return "" } } catch { - Write-Verbose "DSA CNG detection failed: $($_.Exception.Message)" + Write-Information "[VERBOSE] DSA CNG detection failed: $($_.Exception.Message)" } - Write-Verbose "No supported key type detected; provider name could not be determined" + Write-Information "[VERBOSE] No supported key type detected; provider name could not be determined" return "" } catch { @@ -1868,7 +1875,7 @@ function Get-CertificateCSP { function Get-CryptoProviders { # Retrieves the list of available Crypto Service Providers using certutil try { - Write-Verbose "Retrieving Crypto Service Providers using certutil..." + Write-Information "[VERBOSE] Retrieving Crypto Service Providers using certutil..." $certUtilOutput = certutil -csplist # Parse the output to extract CSP names @@ -1884,8 +1891,8 @@ function Get-CryptoProviders { throw "No Crypto Service Providers were found. Ensure certutil is functioning properly." } - Write-Verbose "Retrieved the following CSPs:" - $cspInfoList | ForEach-Object { Write-Verbose $_ } + Write-Information "[VERBOSE] Retrieved the following CSPs:" + $cspInfoList | ForEach-Object { Write-Information "[VERBOSE] $_" } return $cspInfoList } catch { @@ -1899,7 +1906,7 @@ function Validate-CryptoProvider { [Parameter(Mandatory)] [string]$ProviderName ) - Write-Verbose "Validating CSP: $ProviderName" + Write-Information "[VERBOSE] Validating CSP: $ProviderName" $availableProviders = Get-CryptoProviders @@ -1908,10 +1915,11 @@ function Validate-CryptoProvider { throw "Crypto Service Provider '$ProviderName' is either invalid or not found on this system." } - Write-Verbose "Crypto Service Provider '$ProviderName' is valid." + Write-Information "[VERBOSE] Crypto Service Provider '$ProviderName' is valid." } -function Parse-DNSubject { +# Migrated +function Convert-DNSubject { <# .SYNOPSIS Parses a Distinguished Name (DN) subject string and properly quotes RDN values containing escaped commas. @@ -2030,22 +2038,22 @@ function Get-ValidSslFlagsForSystem { # Return array of valid flag values based on Windows Server version if ($build -ge 20348) { # Windows Server 2022+ (IIS 10.0.20348+) - Write-Verbose "Detected Windows Server 2022 or later (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2022 or later (Build: $build)" return @(1, 4, 8, 16, 32, 64) # Include unknowns for testing } elseif ($build -ge 17763) { # Windows Server 2019 (IIS 10.0.17763) - Write-Verbose "Detected Windows Server 2019 (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2019 (Build: $build)" return @(1, 4, 8) } elseif ($build -ge 14393) { # Windows Server 2016 (IIS 10.0) - Write-Verbose "Detected Windows Server 2016 (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2016 (Build: $build)" return @(1, 4) } else { # Windows Server 2012 R2 and earlier (IIS 8.5) - Write-Verbose "Detected Windows Server 2012 R2 or earlier (Build: $build)" + Write-Information "[VERBOSE] Detected Windows Server 2012 R2 or earlier (Build: $build)" return @(1, 2) } } @@ -2119,7 +2127,7 @@ function Test-ValidSslFlags { $successMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is valid for this system (Build: $build)." if ($ThrowOnError) { - Write-Verbose $successMsg + Write-Information "[VERBOSE] $successMsg" return $true } else { @@ -2178,7 +2186,7 @@ function Get-IISManagementInfo { ) $hasIISDrive = Ensure-IISDrive - Write-Verbose "IIS Drive available: $hasIISDrive" + Write-Information "[VERBOSE] IIS Drive available: $hasIISDrive" if ($hasIISDrive) { $null = Import-Module WebAdministration diff --git a/IISU/Properties/AssemblyInfo.cs b/IISU/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..74d3562c --- /dev/null +++ b/IISU/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WindowsCertStore.UnitTests")] +[assembly: InternalsVisibleTo("WindowsCertStore.IntegrationTests")] diff --git a/IISU/RemoteSettings.cs b/IISU/RemoteSettings.cs index 12b0d3c7..04f4056d 100644 --- a/IISU/RemoteSettings.cs +++ b/IISU/RemoteSettings.cs @@ -26,8 +26,7 @@ public class RemoteSettings public string ServerUserName { get; set; } public string ServerPassword { get; set; } - public bool UseJEA { get; set; } - public string JEAEndpointName { get; set; } = "keyfactor.wincert"; + public string JEAEndpointName { get; set; } = ""; } } diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index b98e73a8..0583059f 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -56,6 +56,9 @@ Always + + Always + Always diff --git a/WindowsCertStore.IntegrationTests/ClientConnection.cs b/WindowsCertStore.IntegrationTests/ClientConnection.cs index 4099ad3b..47d074b4 100644 --- a/WindowsCertStore.IntegrationTests/ClientConnection.cs +++ b/WindowsCertStore.IntegrationTests/ClientConnection.cs @@ -9,7 +9,9 @@ namespace WindowsCertStore.IntegrationTests public class ClientConnection { public string Machine { get; set; } + public string StoreType { get; set; } = ""; + public string JEAEndpointName { get; set; } = ""; public string Username { get; set; } - public string PrivateKey { get; set; } // SSH private key + public string PrivateKey { get; set; } } } diff --git a/WindowsCertStore.IntegrationTests/Factories/ConnectionFactory.cs b/WindowsCertStore.IntegrationTests/Factories/ConnectionFactory.cs index 75c826bc..6ea286e6 100644 --- a/WindowsCertStore.IntegrationTests/Factories/ConnectionFactory.cs +++ b/WindowsCertStore.IntegrationTests/Factories/ConnectionFactory.cs @@ -1,50 +1,74 @@ -using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace WindowsCertStore.IntegrationTests.Factories { internal class ConnectionFactory { - // Read the list of IP addresses from an environment variable - // Get the credential information from Azure Key Vault or another secure location - public static IEnumerable GetConnection() + private static (string username, string password) GetCredentials() { - // 1. Build configuration to read from appsettings.json - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + string username = Environment.GetEnvironmentVariable("KEYFACTOR_TEST_USER") ?? string.Empty; + string password = Environment.GetEnvironmentVariable("KEYFACTOR_TEST_PASSWORD") ?? string.Empty; - var config = builder.Build(); + if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) + return (username, password); - // 2. Initialize VaultHelper with configuration - var vault = new VaultHelper(config); + try + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .Build(); - // 3. Retrieve connection details from configuration + var vault = new VaultHelper(config); + return (vault.GetSecret("Username"), vault.GetSecret("Password")); + } + catch + { + return (string.Empty, string.Empty); + } + } + + private static IEnumerable LoadConnections() + { var json = File.ReadAllText("servers.json"); - var machines = JsonConvert.DeserializeObject>>(json); + var connections = JsonConvert.DeserializeObject>(json) ?? new(); - foreach (var entry in machines) + var (username, password) = GetCredentials(); + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + yield break; + + foreach (var conn in connections) { - string machineName = entry["Machine"]; - string username = vault.GetSecret("Username"); - string password = vault.GetSecret("Password"); - - yield return new object[] - { - new ClientConnection - { - Machine = machineName, - Username = username, - PrivateKey = password - } - }; + if (string.IsNullOrEmpty(conn.Machine) || conn.Machine.StartsWith('{')) + continue; + + conn.Username = username; + conn.PrivateKey = password; + yield return conn; } } + + public static IEnumerable GetConnection() => + LoadConnections().Select(c => new object[] { c }); + + public static IEnumerable GetIISConnections() => + LoadConnections() + .Where(c => c.StoreType.Equals("WinIIS", StringComparison.OrdinalIgnoreCase)) + .Select(c => new object[] { c }); + + public static IEnumerable GetSQLConnections() => + LoadConnections() + .Where(c => c.StoreType.Equals("WinSQL", StringComparison.OrdinalIgnoreCase)) + .Select(c => new object[] { c }); + + public static IEnumerable GetWinCertConnections() => + LoadConnections() + .Where(c => c.StoreType.Equals("WinCert", StringComparison.OrdinalIgnoreCase)) + .Select(c => new object[] { c }); } } diff --git a/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs b/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs new file mode 100644 index 00000000..e789dc13 --- /dev/null +++ b/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs @@ -0,0 +1,224 @@ +using Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinCert; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Moq; +using Newtonsoft.Json; +using WindowsCertStore.IntegrationTests.Factories; + +namespace WindowsCertStore.IntegrationTests +{ + [Trait("Category", "Integration")] + public class WinCertIntegrationTests + { + private static (string thumbprint, string base64Pfx, string pfxPassword) CreateTestCertificate() + { + string pfxPassword = CertificateFactory.GeneratePfxPassword(); + var (thumbprint, base64Pfx) = CertificateFactory.CreateSelfSignedCert("test.example.com", pfxPassword); + return (thumbprint, base64Pfx, pfxPassword); + } + + private static ManagementJobConfiguration CreateManagementJobConfig( + ClientConnection connection, + string thumbprint, + string base64Pfx, + string pfxPassword, + string alias, + Dictionary managementJobProperties, + string certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType operationType, + bool overwrite) + { + var job = ConfigurationFactory.GetManagementConfig().First(); + job.ServerUsername = connection.Username; + job.ServerPassword = connection.PrivateKey; + job.OperationType = operationType; + job.Overwrite = overwrite; + job.JobCertificate = new ManagementJobCertificate + { + Thumbprint = thumbprint, + Contents = base64Pfx, + Alias = alias, + PrivateKeyPassword = pfxPassword, + ContentsFormat = "PFX" + }; + job.JobProperties = managementJobProperties; + job.Capability = "CertStores.WinCert.Management"; + job.CertificateStoreDetails.ClientMachine = connection.Machine; + job.CertificateStoreDetails.StorePath = "My"; + job.CertificateStoreDetails.Properties = certStorejobProperties; + return job; + } + + private static InventoryJobConfiguration CreateInventoryJobConfig(ClientConnection connection, string certStorejobProperties) + { + var inventoryJob = ConfigurationFactory.GetInventoryConfig().First(); + inventoryJob.ServerUsername = connection.Username; + inventoryJob.ServerPassword = connection.PrivateKey; + inventoryJob.CertificateStoreDetails.ClientMachine = connection.Machine; + inventoryJob.CertificateStoreDetails.StorePath = "My"; + inventoryJob.CertificateStoreDetails.Properties = certStorejobProperties; + inventoryJob.Capability = "CertStores.WinCert.Inventory"; + return inventoryJob; + } + + private static Dictionary GetManagementJobProperties() => new() + { + ["ProviderName"] = "" + }; + + private static string GetCertStoreJobProperties(ClientConnection connection) => + JsonConvert.SerializeObject(new Dictionary + { + ["spnwithport"] = "false", + ["WinRm Protocol"] = "http", + ["WinRm Port"] = "5985", + ["ServerUsername"] = connection.Username, + ["ServerPassword"] = connection.PrivateKey, + ["ServerUseSsl"] = "false", + ["JEAEndpointName"] = connection.JEAEndpointName ?? "" + }); + + private static (bool found, string? alias) FindAliasByThumbprint(IEnumerable inventory, string thumbprint) + { + var matchedItem = inventory + .FirstOrDefault(item => !string.IsNullOrEmpty(item.Alias) && + item.Alias.Equals(thumbprint, StringComparison.OrdinalIgnoreCase)); + return (matchedItem != null, matchedItem?.Alias); + } + + private static void AssertJobResult(Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult result, string? failureMessage) + { + switch (result) + { + case Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure: + Assert.Fail(failureMessage); + break; + case Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success: + case Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Warning: + Assert.True(true); + break; + default: + Assert.Fail("Unexpected job result status."); + break; + } + } + + [Theory] + [MemberData(nameof(ConnectionFactory.GetWinCertConnections), MemberType = typeof(ConnectionFactory))] + public void WinCert_Management_Add_Inventory_Remove_EndToEnd_Test(ClientConnection connection) + { + var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); + var secretResolver = new Mock(); + secretResolver.Setup(m => m.Resolve(It.IsAny())).Returns((string s) => s); + + var managementJobProperties = GetManagementJobProperties(); + var certStorejobProperties = GetCertStoreJobProperties(connection); + + // Add certificate + var addJob = CreateManagementJobConfig( + connection, thumbprint, base64Pfx, pfxPassword, thumbprint, + managementJobProperties, certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType.Add, true); + + var management = new Management(secretResolver.Object); + var result = management.ProcessJob(addJob); + AssertJobResult(result.Result, result.FailureMessage); + + // Inventory + var inventoryJob = CreateInventoryJobConfig(connection, certStorejobProperties); + var inventory = new Inventory(secretResolver.Object); + IEnumerable returnedInventory = new List(); + SubmitInventoryUpdate submitInventoryUpdate = items => + { + returnedInventory = items; + return true; + }; + result = inventory.ProcessJob(inventoryJob, submitInventoryUpdate); + AssertJobResult(result.Result, result.FailureMessage); + + var (thumbprintFound, returnedAlias) = FindAliasByThumbprint(returnedInventory, thumbprint); + Assert.True(thumbprintFound, $"The inventory did not return the expected certificate with thumbprint: {thumbprint}"); + + // Remove certificate + var removeJob = CreateManagementJobConfig( + connection, null, "", "", returnedAlias ?? thumbprint, + managementJobProperties, certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType.Remove, false); + + result = management.ProcessJob(removeJob); + Assert.NotNull(result); + AssertJobResult(result.Result, result.FailureMessage); + } + + [Theory] + [MemberData(nameof(ConnectionFactory.GetWinCertConnections), MemberType = typeof(ConnectionFactory))] + public void WinCert_Management_Add_Inventory_Renewal_Inventory_Remove_EndToEnd_Test(ClientConnection connection) + { + var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); + var secretResolver = new Mock(); + secretResolver.Setup(m => m.Resolve(It.IsAny())).Returns((string s) => s); + + var managementJobProperties = GetManagementJobProperties(); + var certStorejobProperties = GetCertStoreJobProperties(connection); + + // Add original certificate + var addJob = CreateManagementJobConfig( + connection, thumbprint, base64Pfx, pfxPassword, thumbprint, + managementJobProperties, certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType.Add, true); + + var management = new Management(secretResolver.Object); + var result = management.ProcessJob(addJob); + AssertJobResult(result.Result, result.FailureMessage); + + // Inventory + var inventoryJob = CreateInventoryJobConfig(connection, certStorejobProperties); + var inventory = new Inventory(secretResolver.Object); + IEnumerable returnedInventory = new List(); + SubmitInventoryUpdate submitInventoryUpdate = items => + { + returnedInventory = items; + return true; + }; + result = inventory.ProcessJob(inventoryJob, submitInventoryUpdate); + AssertJobResult(result.Result, result.FailureMessage); + + var (thumbprintFound, _) = FindAliasByThumbprint(returnedInventory, thumbprint); + Assert.True(thumbprintFound, $"The inventory did not return the expected certificate with thumbprint: {thumbprint}"); + + // Renew: add new certificate referencing original thumbprint + var (renewalThumbprint, renewalBase64Pfx, renewalPfxPassword) = CreateTestCertificate(); + var renewalJob = CreateManagementJobConfig( + connection, thumbprint, renewalBase64Pfx, renewalPfxPassword, renewalThumbprint, + managementJobProperties, certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType.Add, true); + + result = management.ProcessJob(renewalJob); + AssertJobResult(result.Result, result.FailureMessage); + + // Inventory after renewal + inventoryJob = CreateInventoryJobConfig(connection, certStorejobProperties); + returnedInventory = new List(); + submitInventoryUpdate = items => + { + returnedInventory = items; + return true; + }; + result = inventory.ProcessJob(inventoryJob, submitInventoryUpdate); + AssertJobResult(result.Result, result.FailureMessage); + + var (renewalThumbprintFound, renewalReturnedAlias) = FindAliasByThumbprint(returnedInventory, renewalThumbprint); + Assert.True(renewalThumbprintFound, $"The inventory did not return the renewed certificate with thumbprint: {renewalThumbprint}"); + + // Remove renewed certificate + var removeJob = CreateManagementJobConfig( + connection, null, "", "", renewalReturnedAlias ?? renewalThumbprint, + managementJobProperties, certStorejobProperties, + Keyfactor.Orchestrators.Common.Enums.CertStoreOperationType.Remove, false); + + result = management.ProcessJob(removeJob); + Assert.NotNull(result); + AssertJobResult(result.Result, result.FailureMessage); + } + } +} diff --git a/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs b/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs index d6ab3541..38ee6dff 100644 --- a/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs +++ b/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs @@ -8,6 +8,7 @@ namespace WindowsCertStore.IntegrationTests { + [Trait("Category", "Integration")] public class WinIISIntegrationTests { private static (string thumbprint, string base64Pfx, string pfxPassword) CreateTestCertificate() @@ -81,7 +82,8 @@ private static string GetCertStoreJobProperties(ClientConnection connection) => ["WinRm Port"] = "5985", ["ServerUsername"] = connection.Username, ["ServerPassword"] = connection.PrivateKey, - ["ServerUseSsl"] = "false" + ["ServerUseSsl"] = "false", + ["JEAEndpointName"] = connection.JEAEndpointName ?? "" }); private static (bool found, string? alias) FindAliasByThumbprint(IEnumerable inventory, string thumbprint) @@ -110,7 +112,7 @@ private static void AssertJobResult(Keyfactor.Orchestrators.Common.Enums.Orchest } [Theory] - [MemberData(nameof(ConnectionFactory.GetConnection), MemberType = typeof(ConnectionFactory))] + [MemberData(nameof(ConnectionFactory.GetIISConnections), MemberType = typeof(ConnectionFactory))] public void WinIIS_Management_Add_Inventory_Remove_EndToEnd_Test(ClientConnection connection) { var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); @@ -157,7 +159,7 @@ public void WinIIS_Management_Add_Inventory_Remove_EndToEnd_Test(ClientConnectio } [Theory] - [MemberData(nameof(ConnectionFactory.GetConnection), MemberType = typeof(ConnectionFactory))] + [MemberData(nameof(ConnectionFactory.GetIISConnections), MemberType = typeof(ConnectionFactory))] public void WinIIS_Management_Add_Inventory_Renewal_Inventory_Remove_EndToEnd_Test(ClientConnection connection) { var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); diff --git a/WindowsCertStore.IntegrationTests/WinSQLIntegrationTests.cs b/WindowsCertStore.IntegrationTests/WinSQLIntegrationTests.cs index 00d6cf20..a5427d53 100644 --- a/WindowsCertStore.IntegrationTests/WinSQLIntegrationTests.cs +++ b/WindowsCertStore.IntegrationTests/WinSQLIntegrationTests.cs @@ -7,6 +7,7 @@ namespace WindowsCertStore.IntegrationTests { + [Trait("Category", "Integration")] public class WinSQLIntegrationTests { private static (string thumbprint, string base64Pfx, string pfxPassword) CreateTestCertificate() @@ -76,7 +77,8 @@ private static string GetCertStoreJobProperties(ClientConnection connection) => ["ServerUsername"] = connection.Username, ["ServerPassword"] = connection.PrivateKey, ["ServerUseSsl"] = "false", - ["RestartService"] = "false" + ["RestartService"] = "false", + ["JEAEndpointName"] = connection.JEAEndpointName ?? "" }); private static (bool found, string? alias) FindAliasByThumbprint(IEnumerable inventory, string thumbprint) @@ -105,7 +107,7 @@ private static void AssertJobResult(Keyfactor.Orchestrators.Common.Enums.Orchest } [Theory] - [MemberData(nameof(ConnectionFactory.GetConnection), MemberType = typeof(ConnectionFactory))] + [MemberData(nameof(ConnectionFactory.GetSQLConnections), MemberType = typeof(ConnectionFactory))] public void WinSql_Management_Add_Inventory_Remove_EndToEnd_Test(ClientConnection connection) { var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); @@ -153,8 +155,8 @@ public void WinSql_Management_Add_Inventory_Remove_EndToEnd_Test(ClientConnectio } [Theory] - [MemberData(nameof(ConnectionFactory.GetConnection), MemberType = typeof(ConnectionFactory))] - public void WinCert_Management_Add_Inventory_Renewal_Inventory_Remove_EndToEnd_Test(ClientConnection connection) + [MemberData(nameof(ConnectionFactory.GetSQLConnections), MemberType = typeof(ConnectionFactory))] + public void WinSQL_Management_Add_Inventory_Renewal_Inventory_Remove_EndToEnd_Test(ClientConnection connection) { var (thumbprint, base64Pfx, pfxPassword) = CreateTestCertificate(); var secretResolver = new Mock(); diff --git a/WindowsCertStore.IntegrationTests/servers.json b/WindowsCertStore.IntegrationTests/servers.json index 0078b4d9..f45ae95d 100644 --- a/WindowsCertStore.IntegrationTests/servers.json +++ b/WindowsCertStore.IntegrationTests/servers.json @@ -1,3 +1,8 @@ [ - { "Machine": "{IPAddress}" } + { "Machine": "{IIS-Server-IP-or-Hostname}", "StoreType": "WinIIS", "JEAEndpointName": "" }, + { "Machine": "{IIS-JEA-Server-IP-or-Hostname}", "StoreType": "WinIIS", "JEAEndpointName": "keyfactor.wincert" }, + { "Machine": "{SQL-Server-IP-or-Hostname}", "StoreType": "WinSQL", "JEAEndpointName": "" }, + { "Machine": "{SQL-JEA-Server-IP-or-Hostname}", "StoreType": "WinSQL", "JEAEndpointName": "keyfactor.wincert" }, + { "Machine": "{WinCert-Server-IP-or-Hostname}", "StoreType": "WinCert", "JEAEndpointName": "" }, + { "Machine": "{WinCert-JEA-Server-IP-or-Hostname}", "StoreType": "WinCert", "JEAEndpointName": "keyfactor.wincert" } ] diff --git a/WindowsCertStore.UnitTests/JobPropertiesTests.cs b/WindowsCertStore.UnitTests/JobPropertiesTests.cs new file mode 100644 index 00000000..493d3733 --- /dev/null +++ b/WindowsCertStore.UnitTests/JobPropertiesTests.cs @@ -0,0 +1,76 @@ +using Keyfactor.Extensions.Orchestrator.WindowsCertStore; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace WindowsCertStore.UnitTests +{ + public class JobPropertiesTests + { + private static JobProperties Deserialize(string json) => + JsonConvert.DeserializeObject(json, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }) + ?? new JobProperties(); + + [Fact] + public void JobProperties_JEAEndpointName_DefaultsToEmptyString() + { + var props = Deserialize("{}"); + Assert.Equal("", props.JEAEndpointName); + } + + [Fact] + public void JobProperties_JEAEndpointName_ReadsValue() + { + var props = Deserialize("{\"JEAEndpointName\": \"keyfactor.wincert\"}"); + Assert.Equal("keyfactor.wincert", props.JEAEndpointName); + } + + [Fact] + public void JobProperties_UseJEA_LegacyField_IsIgnored() + { + // JSON with a UseJEA field that no longer exists should deserialize without error + var props = Deserialize("{\"UseJEA\": true, \"JEAEndpointName\": \"\"}"); + Assert.NotNull(props); + Assert.Equal("", props.JEAEndpointName); + } + + [Fact] + public void JobProperties_WinRmProtocol_DefaultsToHttp() + { + var props = Deserialize("{}"); + Assert.Equal("http", props.WinRmProtocol); + } + + [Fact] + public void JobProperties_WinRmPort_DefaultsTo5985() + { + var props = Deserialize("{}"); + Assert.Equal("5985", props.WinRmPort); + } + + [Fact] + public void JobProperties_SpnPortFlag_DefaultsToFalse() + { + var props = Deserialize("{}"); + Assert.False(props.SpnPortFlag); + } + + [Fact] + public void JobProperties_FullJson_ParsesCorrectly() + { + string json = JsonConvert.SerializeObject(new Dictionary + { + ["spnwithport"] = "false", + ["WinRm Protocol"] = "https", + ["WinRm Port"] = "5986", + ["JEAEndpointName"] = "my.jea.endpoint" + }); + + var props = Deserialize(json); + Assert.False(props.SpnPortFlag); + Assert.Equal("https", props.WinRmProtocol); + Assert.Equal("5986", props.WinRmPort); + Assert.Equal("my.jea.endpoint", props.JEAEndpointName); + } + } +} diff --git a/WindowsCertStore.UnitTests/PSHelperConfigTests.cs b/WindowsCertStore.UnitTests/PSHelperConfigTests.cs new file mode 100644 index 00000000..c2798700 --- /dev/null +++ b/WindowsCertStore.UnitTests/PSHelperConfigTests.cs @@ -0,0 +1,43 @@ +using Keyfactor.Extensions.Orchestrator.WindowsCertStore; + +namespace WindowsCertStore.UnitTests +{ + public class PSHelperConfigTests + { + [Fact] + public void PSHelper_EmptyConstructor_DoesNotThrow() + { + var ex = Record.Exception(() => new PSHelper()); + Assert.Null(ex); + } + + [Theory] + [InlineData("")] + [InlineData("keyfactor.wincert")] + [InlineData("custom.jea.endpoint")] + public void PSHelper_ParameterizedConstructor_DoesNotThrow(string jeaEndpoint) + { + var ex = Record.Exception(() => + new PSHelper("http", "5985", false, "localhost", "user", "pass", jeaEndpoint: jeaEndpoint)); + Assert.Null(ex); + } + + [Theory] + [InlineData(null, "testdir", null)] + [InlineData("C:\\nonexistent\\path", "PowerShell", null)] + public void FindScriptsDirectory_ReturnsNullForMissingDirectory(string baseDir, string folderName, string expected) + { + string result = PSHelper.FindScriptsDirectory(baseDir, folderName); + Assert.Equal(expected, result); + } + + [Fact] + public void FindScriptsDirectory_FindsPowerShellFolderFromBaseDirectory() + { + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + string result = PSHelper.FindScriptsDirectory(baseDir, "PowerShell"); + Assert.NotNull(result); + Assert.True(Directory.Exists(result), $"Expected directory to exist: {result}"); + } + } +} diff --git a/WindowsCertStore.UnitTests/PSHelperUnitTests.cs b/WindowsCertStore.UnitTests/PSHelperUnitTests.cs index 0bb1737c..67127359 100644 --- a/WindowsCertStore.UnitTests/PSHelperUnitTests.cs +++ b/WindowsCertStore.UnitTests/PSHelperUnitTests.cs @@ -14,7 +14,7 @@ public void Test_LoadAllScripts() { // Arrange var psHelper = new Keyfactor.Extensions.Orchestrator.WindowsCertStore.PSHelper(); - string scriptsFolder = PSHelper.FindScriptsDirectory(AppDomain.CurrentDomain.BaseDirectory, "PowerShellScripts"); + string scriptsFolder = PSHelper.FindScriptsDirectory(AppDomain.CurrentDomain.BaseDirectory, "PowerShell"); // Act string scripts = psHelper.LoadAllScripts(scriptsFolder); diff --git a/WindowsCertStore.UnitTests/WindowsCertStore.UnitTests.csproj b/WindowsCertStore.UnitTests/WindowsCertStore.UnitTests.csproj index 97d70128..508f5dc4 100644 --- a/WindowsCertStore.UnitTests/WindowsCertStore.UnitTests.csproj +++ b/WindowsCertStore.UnitTests/WindowsCertStore.UnitTests.csproj @@ -12,8 +12,13 @@ + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From c38621da73ff343fafaa2268bc2c64fb76e4e3cb Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 1 May 2026 18:18:55 -0500 Subject: [PATCH 08/53] Testing --- .gitignore | 3 +++ WindowsCertStore.IntegrationTests/servers.json | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4ce6fdde..9cad393c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Local test credentials (never commit) +local.runsettings + # User-specific files *.rsuser *.suo diff --git a/WindowsCertStore.IntegrationTests/servers.json b/WindowsCertStore.IntegrationTests/servers.json index f45ae95d..a96e9214 100644 --- a/WindowsCertStore.IntegrationTests/servers.json +++ b/WindowsCertStore.IntegrationTests/servers.json @@ -1,8 +1,8 @@ [ - { "Machine": "{IIS-Server-IP-or-Hostname}", "StoreType": "WinIIS", "JEAEndpointName": "" }, - { "Machine": "{IIS-JEA-Server-IP-or-Hostname}", "StoreType": "WinIIS", "JEAEndpointName": "keyfactor.wincert" }, + { "Machine": "192.168.230.139", "StoreType": "WinIIS", "JEAEndpointName": "" }, + { "Machine": "192.168.230.139", "StoreType": "WinIIS", "JEAEndpointName": "keyfactor.wincert" }, { "Machine": "{SQL-Server-IP-or-Hostname}", "StoreType": "WinSQL", "JEAEndpointName": "" }, { "Machine": "{SQL-JEA-Server-IP-or-Hostname}", "StoreType": "WinSQL", "JEAEndpointName": "keyfactor.wincert" }, - { "Machine": "{WinCert-Server-IP-or-Hostname}", "StoreType": "WinCert", "JEAEndpointName": "" }, - { "Machine": "{WinCert-JEA-Server-IP-or-Hostname}", "StoreType": "WinCert", "JEAEndpointName": "keyfactor.wincert" } + { "Machine": "192.168.230.139", "StoreType": "WinCert", "JEAEndpointName": "" }, + { "Machine": "192.168.230.139", "StoreType": "WinCert", "JEAEndpointName": "keyfactor.wincert" } ] From 06434355f8fa97c47ad6e6b8fe578b363e3a2095 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 6 May 2026 09:29:01 -0500 Subject: [PATCH 09/53] Removed WinCertScripts from extension. --- .../WinIIS/Management.cs | 2 +- .../Keyfactor.WinCert.IIS.psm1 | 1 + ...Remove-KeyfactorIISCertificateIfUnused.ps1 | 58 + IISU/PowerShell/WinCertScripts.ps1 | 2258 ----------------- IISU/WindowsCertStore.csproj | 3 - 5 files changed, 60 insertions(+), 2262 deletions(-) create mode 100644 IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 delete mode 100644 IISU/PowerShell/WinCertScripts.ps1 diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index b9067c08..7f5ed986 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -331,7 +331,7 @@ public void RemoveIISCertificate(string thumbprint) { "StoreName", _storePath } }; - _psHelper.ExecutePowerShell("Remove-KFIISCertificateIfUnused", parameters); + _psHelper.ExecutePowerShell("Remove-KeyfactorIISCertificateIfUnused", parameters); } diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 index 8e54a0ab..5aba30fc 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 @@ -22,6 +22,7 @@ if (-not (Get-Command 'New-KeyfactorResult' -ErrorAction SilentlyContinue)) { . "$PSScriptRoot\Private\Test-ValidSslFlags.ps1" . "$PSScriptRoot\Private\Add-IISBindingWithSSL.ps1" . "$PSScriptRoot\Private\Get-IISManagementInfo.ps1" +. "$PSScriptRoot\Private\Remove-KeyfactorIISCertificateIfUnused.ps1" # Public functions . "$PSScriptRoot\Public\Get-KeyfactorIISBoundCertificates.ps1" diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 new file mode 100644 index 00000000..d134f4ca --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 @@ -0,0 +1,58 @@ +function Remove-KeyfactorIISCertificateIfUnused { + param ( + [Parameter(Mandatory = $true)] + [string]$Thumbprint, + + [Parameter(Mandatory = $false)] + [string]$StoreName = "My" + ) + + try { + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + } catch { + throw "Failed to load Microsoft.Web.Administration. Ensure IIS is installed on the remote server." + } + + # Normalize thumbprint: strip whitespace and force uppercase for consistent comparison + $normalizedThumbprint = ($Thumbprint -replace '\s', '').ToUpperInvariant() + Write-Information "[VERBOSE] Remove-KeyfactorIISCertificateIfUnused: checking thumbprint $normalizedThumbprint in store $StoreName" + + try { + $serverManager = New-Object Microsoft.Web.Administration.ServerManager + $bindings = @() + + foreach ($site in $serverManager.Sites) { + foreach ($binding in $site.Bindings) { + if ($binding.Protocol -eq 'https' -and $binding.CertificateHash) { + $bindingThumbprint = ($binding.CertificateHash | ForEach-Object { $_.ToString("X2") }) -join "" + if ($bindingThumbprint -eq $normalizedThumbprint) { + $bindings += [PSCustomObject]@{ + SiteName = $site.Name + Binding = $binding.BindingInformation + } + } + } + } + } + + if ($bindings.Count -gt 0) { + Write-Information "[VERBOSE] Certificate $normalizedThumbprint is still active on $($bindings.Count) binding(s) — skipping removal" + $bindings | ForEach-Object { Write-Warning " Still bound: $($_.SiteName) / $($_.Binding)" } + return + } + + $cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" | + Where-Object { $_.Thumbprint -eq $normalizedThumbprint } + + if (-not $cert) { + Write-Information "[VERBOSE] Certificate $normalizedThumbprint not found in Cert:\LocalMachine\$StoreName — nothing to remove" + return + } + + Remove-Item -Path $cert.PSPath -Force + Write-Information "[VERBOSE] Certificate $normalizedThumbprint removed from Cert:\LocalMachine\$StoreName" + } + catch { + Write-Error "An error occurred while attempting to remove IIS certificate: $_" + } +} diff --git a/IISU/PowerShell/WinCertScripts.ps1 b/IISU/PowerShell/WinCertScripts.ps1 deleted file mode 100644 index 0912c6f9..00000000 --- a/IISU/PowerShell/WinCertScripts.ps1 +++ /dev/null @@ -1,2258 +0,0 @@ -# Version 1.5.1 - -# Summary -# Contains PowerShell functions to execute administration jobs for general Windows certificates, IIS and SQL Server. -# There are additional supporting PowerShell functions to support job specific actions. - -# Update notes: -# Date Version Description -# 08/12/25 2.6.x Updated functions to manage IIS bindings and certificates -# Updated script to read CSPs correctly using newer CNG Keys -# Fix an error with complex PFX passwords having irregular characters -# 08/29/25 2.6.x Fixed the add cert to store function to return the correct thumbprint -# Made changes to the IIS Binding logic, breaking it into manageable pieces to aid in debugging issues -# 09/16/25 2.6.3 Updated the Get CSP function to handle null values when reading hybrid certificates -# 11/17/25 2.6.4 Fixed issue with SSL Flags not being applied correctly to IIS bindings -# 11/21/25 Renamed Set-KFCertificateBinding to Set-KFSQLCertificateBinding -# Fixed the Set-KFSQLCertificateBinding function to correctly bind and set the ACL permissions on the private key when using Windows-to-Windows and SSH-based remote connections. -# Updated the Set-KFSQLCertificateBinding to handle both CNG (modern) and CAPI (legacy) certificate key storage providers when setting ACLs on private keys. -# 10/08/25 3.0 Updated the Get-KFIISBoundCertificates function to fixed the SSL flag not returning the correct value when reading IIS bindings -# Updated the New-KFIISSiteBinding to correctly update the SSL flags -# Added Test-ValidSslFlags to verify the correct bit flag -# 11/04/25 3.0 Updated Get-KFCertificates to get specific certificate by thumbprint -# 01/14/26 3.0 Released version 3.0.0 with updated function documentation and error handling -# 01/20/26 3.0 Fixed a problem for invalid SSL flags depending on the version of IIS and Windows Server is being used -# 03/18/26 3.0.1 Updated the Get-CertificateCSP that adds ECC (ECDsa) key detection, consolidates the CNG provider lookup into a reusable helper, and handles DSA as a bonus - -# Set preferences globally at the script level -$DebugPreference = "Continue" -$VerbosePreference = "Continue" -$InformationPreference = "Continue" - -#Standard Step Names -# Step Name Purpose -# ValidateInput Validate required params and input data -# FindSite Checking if the IIS site exists -# CheckBinding Looking up existing bindings -# RemoveBinding Attempting to remove an old binding -# AddBinding Adding the new IIS binding -# LoadCertificate Fetching or validating the SSL certificate -# CompareThumbprint Checking if binding needs to be updated -# BindSSL Adding SSL cert to a binding -# ImportModules Importing IIS-related PowerShell modules -# CatchAll Fallback for unexpected or generic errors - -# Standard Error Codes -#Code Status Description -# 0 Success Operation completed successfully -# 100 Skipped Binding already exists and is up-to-date -# 101 Warning Binding exists but is invalid -# 200 Error Site not found -# 201 Error Failed to remove binding -# 202 Error Failed to add binding -# 203 Error Certificate not found -# 204 Error Certificate already in use elsewhere -# 205 Error Thumbprint mismatch -# 206 Error WebAdministration module missing -# 207 Error IISAdministration module missing -# 300 Error Unknown or unhandled exception -# 400 Error Invalid Ssl Flag bit combination - -# Migrated -function New-ResultObject { - param( - [ValidateSet("Success", "Warning", "Error", "Skipped")] - [string]$Status, - [int]$Code, - [string]$Step, - [string]$Message, - [string]$ErrorMessage = "", - [hashtable]$Details = @{} - ) - - return [PSCustomObject]@{ - Status = $Status - Code = $Code - Step = $Step - Message = $Message - ErrorMessage = $ErrorMessage - Details = $Details - } -} - -# Migrated -function Get-KFCertificates { - param ( - [Parameter(Mandatory = $false)] - [string]$StoreName = "My", # Default store name is "My" (Personal) - - [Parameter(Mandatory = $false)] - [string]$Thumbprint # Optional: specific certificate thumbprint to retrieve - ) - - # Define the store path using the provided StoreName parameter - $storePath = "Cert:\LocalMachine\$StoreName" - - # Check if the store path exists to ensure the store is valid - if (-not (Test-Path $storePath)) { - # Write an error message and exit the function if the store path is invalid - Write-Error "The certificate store path '$storePath' does not exist. Please provide a valid store name." - return - } - - # Retrieve certificates from the specified store - if ($Thumbprint) { - # If thumbprint is provided, retrieve only that specific certificate - # Remove any spaces or special characters from the thumbprint for comparison - $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' - $certificates = Get-ChildItem -Path $storePath | Where-Object { - ($_.Thumbprint -replace '[^a-fA-F0-9]', '') -eq $cleanThumbprint - } - - if (-not $certificates) { - Write-Error "No certificate found with thumbprint '$Thumbprint' in store '$StoreName'." - return - } - } else { - # Retrieve all certificates from the specified store - $certificates = Get-ChildItem -Path $storePath - } - - # Initialize an empty array to store certificate information objects - $certInfoList = @() - - foreach ($cert in $certificates) { - try { - # Create a custom object to store details about the current certificate - $certInfo = [PSCustomObject]@{ - StoreName = $StoreName # Name of the certificate store - Certificate = $cert.Subject # Subject of the certificate - ExpiryDate = $cert.NotAfter # Expiration date of the certificate - Issuer = $cert.Issuer # Issuer of the certificate - Thumbprint = $cert.Thumbprint # Unique thumbprint of the certificate - HasPrivateKey = $cert.HasPrivateKey # Indicates if the certificate has a private key - SAN = Get-KFSAN $cert # Subject Alternative Names (if available) - ProviderName = Get-CertificateCSP $cert # Provider of the certificate - Base64Data = [System.Convert]::ToBase64String($cert.RawData) # Encoded raw certificate data - } - - # Add the certificate information object to the results array - $certInfoList += $certInfo - } catch { - # Write a warning message if there is an error processing the current certificate - Write-Warning "An error occurred while processing the certificate: $_" - } - } - - # Output the results in JSON format if certificates were found - if ($certInfoList) { - $certInfoList | ConvertTo-Json -Depth 10 - } else { - # Write a warning if no certificates were found in the specified store - Write-Warning "No certificates were found in the store '$StoreName'." - } -} - -# Migrated -function Add-KFCertificateToStore{ - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$PrivateKeyPassword, - - [Parameter(Mandatory = $true)] - [string]$StoreName, - - [Parameter(Mandatory = $false)] - [string]$CryptoServiceProvider - ) - - try { - Write-Information "Entering PowerShell Script Add-KFCertificate" - Write-Information "[VERBOSE] Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" - - # Get the thumbprint of the passed in certificate - # Convert password to secure string if provided, otherwise use $null - $bytes = [System.Convert]::FromBase64String($Base64Cert) - $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } - - # Set the storage flags and get the certificate's thumbprint - $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor ` - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet - - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, $keyStorageFlags) - $thumbprint = $cert.Thumbprint - - if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } - - if ($CryptoServiceProvider) - { - # Test to see if CSP exists - if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) - { - Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system." - Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system." - return - } - - Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" - - # Create temporary file for the PFX - $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx" - [System.IO.File]::WriteAllBytes($tempPfx, [Convert]::FromBase64String($Base64Cert)) - - - # Execute certutil based on whether a private key password was supplied - try { - # Start building certutil arguments - $arguments = @('-f') - - if ($PrivateKeyPassword) { - Write-Information "[VERBOSE] Has a private key" - $arguments += '-p' - $arguments += $PrivateKeyPassword - } - - if ($CryptoServiceProvider) { - Write-Information "[VERBOSE] Has a CryptoServiceProvider: $CryptoServiceProvider" - $arguments += '-csp' - $arguments += $CryptoServiceProvider - } - - $arguments += '-importpfx' - $arguments += $StoreName - $arguments += $tempPfx - - # Quote any arguments with spaces - $argLine = ($arguments | ForEach-Object { - if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } - }) -join ' ' - - Write-Information "[VERBOSE] Running certutil with arguments: $argLine" - - # Setup process execution - $processInfo = New-Object System.Diagnostics.ProcessStartInfo - $processInfo.FileName = "certutil.exe" - $processInfo.Arguments = $argLine.Trim() - $processInfo.RedirectStandardOutput = $true - $processInfo.RedirectStandardError = $true - $processInfo.UseShellExecute = $false - $processInfo.CreateNoWindow = $true - - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $processInfo - - $process.Start() | Out-Null - - $stdOut = $process.StandardOutput.ReadToEnd() - $stdErr = $process.StandardError.ReadToEnd() - - $process.WaitForExit() - - if ($process.ExitCode -ne 0) { - throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" - } - } catch { - Write-Error "ERROR: $_" - } finally { - if (Test-Path $tempPfx) { - Remove-Item $tempPfx -Force - } - } - - } else { - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" - Write-Information "Store '$StoreName' is open." - - # Open store with read/write, and don't create the store if it doesn't exist - $openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite -bor ` - [System.Security.Cryptography.X509Certificates.OpenFlags]::OpenExistingOnly - $certStore.Open($openFlags) - $certStore.Add($cert) - $certStore.Close(); - Write-Information "Store '$StoreName' is closed." - } - - Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." - return $thumbprint - } catch { - Write-Error "An error occurred: $_" - return $null - } -} - -# Migrated -function Remove-KFCertificateFromStore { - param ( - [string]$Thumbprint, - [string]$StorePath, - - [parameter(ParameterSetName = $false)] - [switch]$IsAlias - ) - - # Initialize a variable to track success - $isSuccessful = $false - - try { - # Open the certificate store - $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($StorePath, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) - $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - - # Find the certificate by thumbprint or alias - if ($IsAlias) { - $cert = $store.Certificates | Where-Object { $_.FriendlyName -eq $Thumbprint } - } else { - $cert = $store.Certificates | Where-Object { $_.Thumbprint -eq $Thumbprint } - } - - if ($cert) { - # Remove the certificate from the store - Write-Information "Attempting to remove certificate from store '$StorePath' with the thumbprint: $Thumbprint" - $store.Remove($cert) - Write-Information "Certificate removed successfully from store '$StorePath'" - - # Mark success - $isSuccessful = $true - } else { - throw [System.Exception]::new("Certificate not found in $StorePath.") - } - - # Close the store - $store.Close() - } catch { - # Log and rethrow the exception - Write-Error "An error occurred: $_" - throw $_ - } finally { - # Ensure the store is closed - if ($store) { - $store.Close() - } - } - - # Return the success status - return $isSuccessful -} - -##### IIS Functions -#Migrated -function Get-KFIISBoundCertificates { - $certificates = @() - $totalBoundCertificates = 0 - - try { - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" # -AssemblyName "Microsoft.Web.Administration" - $serverManager = New-Object Microsoft.Web.Administration.ServerManager - } catch { - Write-Error "Failed to create ServerManager. IIS might not be installed." - return - } - - $websites = $serverManager.Sites - Write-Information "There were $($websites.Count) websites found." - - foreach ($site in $websites) { - $siteName = $site.Name - $siteBoundCertificateCount = 0 - - foreach ($binding in $site.Bindings) { - if ($binding.Protocol -eq 'https' -and $binding.CertificateHash) { - $certHash = ($binding.CertificateHash | ForEach-Object { $_.ToString("X2") }) -join "" - $storeName = if ($binding.CertificateStoreName) { $binding.CertificateStoreName } else { "My" } - - try { - $cert = Get-ChildItem -Path "Cert:\LocalMachine\$storeName" | Where-Object { - $_.Thumbprint -eq $certHash - } - - if (-not $cert) { - Write-Warning "Certificate with thumbprint not found in Cert:\LocalMachine\$storeName" - continue - } - - $certBase64 = [Convert]::ToBase64String($cert.RawData) - $ip, $port, $hostname = $binding.BindingInformation -split ":", 3 - - $certInfo = [PSCustomObject]@{ - SiteName = $siteName - Binding = $binding.BindingInformation - IPAddress = $ip - Port = $port - Hostname = $hostname - Protocol = $binding.Protocol - SNI = $binding.SslFlags - ProviderName = Get-CertificateCSP $cert - SAN = Get-KFSAN $cert - Certificate = $cert.Subject - ExpiryDate = $cert.NotAfter - Issuer = $cert.Issuer - Thumbprint = $cert.Thumbprint - HasPrivateKey = $cert.HasPrivateKey - CertificateBase64 = $certBase64 - } - - $certificates += $certInfo - $siteBoundCertificateCount++ - $totalBoundCertificates++ - } catch { - Write-Warning "Could not retrieve certificate details for hash $certHash in store $storeName." - Write-Warning $_ - } - } - } - - Write-Information "Website: $siteName has $siteBoundCertificateCount bindings with certificates." - } - - Write-Information "A total of $totalBoundCertificates bindings with valid certificates were found." - - if ($totalBoundCertificates -gt 0) { - $certificates | ConvertTo-Json - } else { - Write-Information "No valid certificates were found bound to websites." - } -} - -# Migrated -function New-KFIISSiteBinding { - [CmdletBinding()] - [OutputType([pscustomobject])] - param ( - [Parameter(Mandatory = $true)] - [string]$SiteName, - [string]$IPAddress = "*", - [int]$Port = 443, - [AllowEmptyString()] - [string]$Hostname = "", - [ValidateSet("http", "https")] - [string]$Protocol = "https", - [ValidateScript({ - if ($Protocol -eq 'https' -and [string]::IsNullOrEmpty($_)) { - throw "Thumbprint is required when Protocol is 'https'" - } - $true - })] - [string]$Thumbprint, - [string]$StoreName = "My", - [int]$SslFlags = 0 - ) - - Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue - Write-Information "[VERBOSE] Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" - - try { - # Step 1: Perform verifications and get management info - # Check SslFlags - $sslValidationResult = Test-ValidSslFlags -Flags $SslFlags - if (-not $sslValidationResult.IsValid) { - return New-ResultObject -Status Error 400 -Step "SSL Validation" -ErrorMessage $sslValidationResult.Message - } - - $managementInfo = Get-IISManagementInfo -SiteName $SiteName - if (-not $managementInfo.Success) { - return $managementInfo.Result - } - - # Step 2: Remove existing HTTPS bindings for this binding info - $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Information "[VERBOSE] Removing existing HTTPS bindings for: $searchBindings" - - $removalResult = Remove-ExistingIISBinding -SiteName $SiteName -BindingInfo $searchBindings -UseIISDrive $managementInfo.UseIISDrive - if ($removalResult.Status -eq 'Error') { - return $removalResult - } - - # Step 3: Determine SslFlags supported by Microsoft.Web.Administration - if ($SslFlags -gt 3) { - Write-Information "[VERBOSE] SslFlags value $SslFlags exceeds managed API range (0–3). Applying reduced flags for creation." - $SslFlagsApplied = ($SslFlags -band 3) - } else { - $SslFlagsApplied = $SslFlags - } - - # Step 4: Add the new binding with the reduced flag set - Write-Information "[VERBOSE] Adding new binding with SSL certificate (SslFlagsApplied=$SslFlagsApplied)" - - $addParams = @{ - SiteName = $SiteName - Protocol = $Protocol - IPAddress = $IPAddress - Port = $Port - Hostname = $Hostname - Thumbprint = $Thumbprint - StoreName = $StoreName - SslFlags = $SslFlagsApplied - UseIISDrive = $managementInfo.UseIISDrive - } - - $addResult = Add-IISBindingWithSSL @addParams - - if ($addResult.Status -eq 'Error') { - return $addResult - } - - # Step 5: If extended flags, update via appcmd.exe - if ($SslFlags -gt 3) { - Write-Information "[VERBOSE] Applying full SslFlags=$SslFlags via appcmd" - - $appcmd = Join-Path $env:windir "System32\inetsrv\appcmd.exe" - - # Escape any single quotes in hostname - $safeHostname = $Hostname -replace "'", "''" - $bindingInfo = "${IPAddress}:${Port}:${safeHostname}" - - # Quote site name only if it contains spaces - if ($SiteName -match '\s') { - $siteArg = "/site.name:`"$SiteName`"" - } else { - $siteArg = "/site.name:$SiteName" - } - - # Build binding argument for appcmd - $bindingArg = "/bindings.[protocol='https',bindingInformation='$bindingInfo'].sslFlags:$SslFlags" - - Write-Information "[VERBOSE] Running appcmd: $appcmd $siteArg $bindingArg" - $appcmdOutput = & $appcmd set site $siteArg $bindingArg 2>&1 - Write-Information "[VERBOSE] appcmd output: $appcmdOutput" - - #& $appcmd set site $siteArg $bindingArg | Out-Null - - if ($LASTEXITCODE -ne 0) { - Write-Warning "appcmd failed to set extended SslFlags ($SslFlags) for binding $bindingInfo." - } else { - Write-Information "[VERBOSE] Successfully updated SslFlags to $SslFlags via appcmd." - } - } - - return $addResult - } - catch { - $errorMessage = "Unexpected error in New-KFIISSiteBinding: $($_.Exception.Message)" - Write-Error $errorMessage - return New-ResultObject -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage - } -} - -# Migrated -function Remove-ExistingIISBinding { - [CmdletBinding()] - [OutputType([pscustomobject])] - param ( - [Parameter(Mandatory = $true)] - [string]$SiteName, - - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string]$BindingInfo, - - [Parameter(Mandatory = $true)] - [bool]$UseIISDrive - ) - - Write-Information "[VERBOSE] Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" - - try { - if ($UseIISDrive) { - Write-Information "[VERBOSE] Using IIS Drive to remove binding" - $sitePath = "IIS:\Sites\$SiteName" - $site = Get-Item $sitePath - $httpsBindings = $site.Bindings.Collection | Where-Object { - $_.bindingInformation -eq $BindingInfo -and $_.protocol -eq "https" - } - - foreach ($binding in $httpsBindings) { - $bindingInfo = $binding.GetAttributeValue("bindingInformation") - $protocol = $binding.protocol - - Write-Information "[VERBOSE] Removing binding: $bindingInfo ($protocol)" - Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false - Write-Information "[VERBOSE] Successfully removed binding" - } - } - else { - Write-Information "[VERBOSE] Using Web Administration assembly to remove binding" - # ServerManager fallback - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager - $site = $iis.Sites[$SiteName] - - $httpsBindings = $site.Bindings | Where-Object { - $_.BindingInformation -eq $BindingInfo -and $_.Protocol -eq "https" - } - - foreach ($binding in $httpsBindings) { - Write-Information "[VERBOSE] Removing binding: $($binding.BindingInformation)" - $site.Bindings.Remove($binding) - Write-Information "[VERBOSE] Successfully removed binding" - } - - $iis.CommitChanges() - Write-Information "[VERBOSE] Committed changes to IIS" - } - - return New-ResultObject -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" - } - catch { - $errorMessage = "Error removing existing binding: $($_.Exception.Message)" - Write-Warning $errorMessage - return New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $errorMessage - } -} - -# Migrated -function Add-IISBindingWithSSL { - [CmdletBinding()] - [OutputType([pscustomobject])] - param ( - [Parameter(Mandatory = $true)] - [string]$SiteName, - - [Parameter(Mandatory = $true)] - [string]$Protocol, - - [Parameter(Mandatory = $true)] - [string]$IPAddress, - - [Parameter(Mandatory = $true)] - [int]$Port, - - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string]$Hostname, - - [string]$Thumbprint, - - [string]$StoreName = "My", - - [int]$SslFlags = 0, - - [Parameter(Mandatory = $true)] - [bool]$UseIISDrive - ) - - Write-Information "[VERBOSE] Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" - - try { - if ($UseIISDrive) { - # Add binding using WebAdministration module - $bindingParams = @{ - Name = $SiteName - Protocol = $Protocol - IPAddress = $IPAddress - Port = $Port - SslFlags = $SslFlags - } - - # Only add HostHeader if it's not empty (New-WebBinding doesn't like empty strings) - if (-not [string]::IsNullOrEmpty($Hostname)) { - $bindingParams.HostHeader = $Hostname - } - - Write-Information "[VERBOSE] Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" - New-WebBinding @bindingParams - - # Bind SSL certificate if HTTPS - if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { - $searchBindings = "${IPAddress}:${Port}:${Hostname}" - Write-Information "[VERBOSE] Searching for binding: $searchBindings" - - $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { - $_.bindingInformation -eq $searchBindings - } - - if ($binding) { - Write-Information "[VERBOSE] Binding SSL certificate with thumbprint: $Thumbprint" - $null = $binding.AddSslCertificate($Thumbprint, $StoreName) - Write-Information "[VERBOSE] SSL certificate successfully bound" - return New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and SSL certificate successfully applied" - } else { - return New-ResultObject -Status Error -Code 202 -Step BindSSL -ErrorMessage "No binding found for: $searchBindings" - } - } - else { - return New-ResultObject -Status Success -Code 0 -Step AddBinding -Message "HTTP binding successfully added" - } - } - else { - # ServerManager fallback - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager - $site = $iis.Sites[$SiteName] - - $searchBindings = "${IPAddress}:${Port}:${Hostname}" - $newBinding = $site.Bindings.Add($searchBindings, $Protocol) - - if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { - # Clean and convert thumbprint to byte array - $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' - $hashBytes = for ($i = 0; $i -lt $cleanThumbprint.Length; $i += 2) { - [Convert]::ToByte($cleanThumbprint.Substring($i, 2), 16) - } - - $newBinding.CertificateStoreName = $StoreName - $newBinding.CertificateHash = [byte[]]$hashBytes - $newBinding.SetAttributeValue("sslFlags", $SslFlags) - } - - $iis.CommitChanges() - return New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and certificate successfully applied via ServerManager" - } - } - catch { - $errorMessage = "Error adding binding with SSL: $($_.Exception.Message)" - Write-Warning $errorMessage - return New-ResultObject -Status Error -Code 202 -Step AddBinding -ErrorMessage $errorMessage - } -} - -# May want to replace this function with Remove-ExistingIISBinding in future version, this is currently being called on certificate removal only -function Remove-KFIISSiteBinding { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] [string] $SiteName, - [Parameter(Mandatory = $true)] [string] $IPAddress, - [Parameter(Mandatory = $true)] [int] $Port, - [Parameter(Mandatory = $false)] [string] $Hostname - ) - - Write-Information "[VERBOSE] Entering PowerShell Scrip Remove-KFIISiteBinding with arguments Sitename: '$SiteName', IP Address: '$IPAddress', Port: $Port, Hostname: '$Hostname'" - - try { - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - } catch { - throw "Failed to load Microsoft.Web.Administration. Ensure IIS is installed on the remote server." - } - - try { - $serverManager = New-Object Microsoft.Web.Administration.ServerManager - $site = $serverManager.Sites | Where-Object { $_.Name -eq $SiteName } - - if (-not $site) { - Write-Information "Site '$SiteName' not found." - return $true - } - - $searchBindingInfo = if ($HostName) { "$IPAddress`:$Port`:$HostName" } else { "$IPAddress`:$Port`:" } - - Write-Information "[VERBOSE] Searching Site for bindings: $searchBindingInfo" - $httpsBinding = $site.Bindings | Where-Object { $_.bindingInformation -eq $searchBindingInfo -and $_.protocol -eq 'https' } - - if ($httpsBinding) - { - $site.Bindings.Remove($httpsBinding) - $serverManager.CommitChanges() - Write-Information "[VERBOSE] Removed binding $httpsBinding from site '$SiteName'." - - return $true - } - else - { - Write-Information "No matching binding found for $searchBindingInfo in site '$SiteName'." - return $true - } - } catch { - Write-Error "An error occurred while removing the binding." - Write-Error $_ - throw - } -} - -# Called on a renewal to remove any certificates if not bound or used -function Remove-KFIISCertificateIfUnused { - param ( - [Parameter(Mandatory = $true)] - [string]$Thumbprint, - - [Parameter(Mandatory = $false)] - [string]$StoreName = "My" # Default to the personal store - - ) - - try { - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - } catch { - throw "Failed to load Microsoft.Web.Administration. Ensure IIS is installed on the remote server." - } - - # Normalize thumbprint (remove spaces and make uppercase) - $thumbprint = $Thumbprint -replace '\s', '' | ForEach-Object { $_.ToUpperInvariant() } - - try { - $serverManager = New-Object Microsoft.Web.Administration.ServerManager - - $bindings = @() - - foreach ($site in $serverManager.Sites) { - foreach ($binding in $site.Bindings) { - if ($binding.Protocol -eq 'https' -and $binding.CertificateHash) { - $bindingThumbprint = ($binding.CertificateHash | ForEach-Object { $_.ToString("X2") }) -join "" - if ($bindingThumbprint -eq $thumbprint) { - $bindings += [PSCustomObject]@{ - SiteName = $site.Name - Binding = $binding.BindingInformation - } - } - } - } - } - - if ($bindings.Count -gt 0) { - Write-Warning "The certificate with thumbprint $thumbprint is still used by the following bindings:" - $bindings | Format-Table -AutoSize | Out-String | Write-Warning - return - } - - # Certificate is not used in any bindings - $cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" | Where-Object { $_.Thumbprint -eq $thumbprint } - - if (-not $cert) { - Write-Warning "Certificate with thumbprint $thumbprint not found in Cert:\LocalMachine\$StoreName" - return - } - - Remove-Item -Path $cert.PSPath -Force - Write-Information "Certificate $thumbprint has been removed from the store." - - } catch { - Write-Error "An error occurred while attempting to delete IIS Certificate: $_" - } -} -##### - -# Function to get certificate information for a SQL Server instance -# Migrated -function GET-KFSQLInventory { - # Retrieve all SQL Server instances - $sqlInstances = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server").InstalledInstances - Write-Information "There are $($sqlInstances.Count) instances that will be checked for certificates." - - # Dictionary to store instance names by thumbprint - $commonInstances = @{} - - # First loop: gather thumbprints for each instance - foreach ($instance in $sqlInstances) { - Write-Information "Checking instance: $instance for Certificates." - - # Get the registry path for the SQL instance - $fullInstanceName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance - $regLocation = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$fullInstanceName\MSSQLServer\SuperSocketNetLib" - - try { - # Retrieve the certificate thumbprint from the registry - $thumbprint = (Get-ItemPropertyValue -Path $regLocation -Name "Certificate" -ErrorAction Stop).ToUpper() - - if ($thumbprint) { - # Store instance names by thumbprint - if ($commonInstances.ContainsKey($thumbprint)) { - $commonInstances[$thumbprint] += ",$instance" - } else { - $commonInstances[$thumbprint] = $instance - } - } - } catch { - Write-Information "No certificate found for instance: $instance." - } - } - - # Array to store results - $myBoundCerts = @() - - # Second loop: process each unique thumbprint and gather certificate data - foreach ($kp in $commonInstances.GetEnumerator()) { - $thumbprint = $kp.Key - $instanceNames = $kp.Value - - # Find the certificate in the local machine store - $certStore = "My" - $cert = Get-ChildItem -Path "Cert:\LocalMachine\$certStore\$thumbprint" -ErrorAction SilentlyContinue - - if ($cert) { - # Create a hashtable with the certificate parameters - $sqlSettingsDict = @{ - InstanceName = $instanceNames - ProviderName = $cert.PrivateKey.CspKeyContainerInfo.ProviderName - } - - # Build the inventory item for this certificate - $inventoryItem = [PSCustomObject]@{ - Certificates = [Convert]::ToBase64String($cert.RawData) - Alias = $thumbprint - PrivateKeyEntry = $cert.HasPrivateKey - UseChainLevel = $false - ItemStatus = "Unknown" # OrchestratorInventoryItemStatus.Unknown equivalent - Parameters = $sqlSettingsDict - } - - # Add the inventory item to the results array - $myBoundCerts += $inventoryItem - } - } - - # Return the array of inventory items - return $myBoundCerts | ConvertTo-Json -} - -# Migrated to New-KeyfactorSQLCertificate -function Bind-KFSqlCertificate { - param ( - [string]$SQLInstance, - [string]$RenewalThumbprint, - [string]$NewThumbprint, - [switch]$RestartService = $false - ) - - function Get-SqlCertRegistryLocation($InstanceName) { - return "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$InstanceName\MSSQLServer\SuperSocketNetLib" - } - - $bindingSuccess = $true # Track success/failure - - try { - $SQLInstances = $SQLInstance -split ',' | ForEach-Object { $_.Trim() } - - foreach ($instance in $SQLInstances) { - try { - $fullInstance = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance -ErrorAction Stop - $regLocation = Get-SqlCertRegistryLocation -InstanceName $fullInstance - - if (-not (Test-Path $regLocation)) { - Write-Error "Registry location not found: $regLocation" - $bindingSuccess = $false - continue - } - Write-Information "[VERBOSE] Instance: $instance" - Write-Information "[VERBOSE] Full Instance: $fullInstance" - Write-Information "[VERBOSE] Registry Location: $regLocation" - Write-Information "[VERBOSE] Current Thumbprint: $currentThumbprint" - - $currentThumbprint = Get-ItemPropertyValue -Path $regLocation -Name "Certificate" -ErrorAction SilentlyContinue - - if ($RenewalThumbprint -and $RenewalThumbprint -contains $currentThumbprint) { - Write-Information "Renewal thumbprint matches for instance: $fullInstance" - $result = Set-KFSQLCertificateBinding -InstanceName $instance -NewThumbprint $NewThumbprint -RestartService:$RestartService - } elseif (-not $RenewalThumbprint) { - Write-Information "No renewal thumbprint provided. Binding certificate to instance: $fullInstance" - $result = Set-KFSQLCertificateBinding -InstanceName $instance -NewThumbprint $NewThumbprint -RestartService:$RestartService - } - - if (-not $result) { - Write-Error "Failed to bind certificate for instance: $instance" - $bindingSuccess = $false - } - } - catch { - Write-Error "Error processing instance '$instance': $($_.Exception.Message)" - $bindingSuccess = $false - } - } - } - catch { - Write-Error "An unexpected error occurred: $($_.Exception.Message)" - return $false - } - - return $bindingSuccess -} - -# Migrated -function Set-KFSQLCertificateBinding { - <# - .SYNOPSIS - Binds a certificate to a SQL Server instance and sets appropriate permissions. - - .DESCRIPTION - This function binds a certificate to a SQL Server instance by updating the registry, - setting ACL permissions on the private key, and optionally restarting the SQL service. - Supports both local Windows-to-Windows and SSH-based remote connections. - Handles both CNG (modern) and CAPI (legacy) certificate key storage. - - .PARAMETER InstanceName - The SQL Server instance name (e.g., "MSSQLSERVER" for default instance) - - .PARAMETER NewThumbprint - The thumbprint of the certificate to bind - - .PARAMETER RestartService - Switch to restart the SQL Server service after binding - - .EXAMPLE - Set-KFSQLCertificateBinding -InstanceName "MSSQLSERVER" -NewThumbprint "ABC123..." -RestartService - #> - - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string]$InstanceName, - - [Parameter(Mandatory = $true)] - [string]$NewThumbprint, - - [switch]$RestartService - ) - - Write-Information "Starting certificate binding process for instance: $InstanceName" - Write-Information "Target certificate thumbprint: $NewThumbprint" - - try { - # ============================================================ - # STEP 1: Get SQL Instance Registry Path - # ============================================================ - Write-Information "Retrieving SQL Server instance information..." - - try { - $fullInstance = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $InstanceName -ErrorAction Stop - $RegistryPath = Get-SqlCertRegistryLocation -InstanceName $fullInstance - - Write-Information "[VERBOSE] Full instance name: $fullInstance" - Write-Information "[VERBOSE] Registry path: $RegistryPath" - Write-Information "SQL Server instance registry path located: $RegistryPath" - } - catch { - Write-Error "Failed to locate SQL Server instance '$InstanceName' in registry: $_" - throw $_ - } - - # ============================================================ - # STEP 2: Update Registry with New Certificate Thumbprint - # ============================================================ - Write-Information "Updating registry with new certificate thumbprint..." - - try { - # Backup current value - $currentThumbprint = Get-ItemPropertyValue -Path $RegistryPath -Name "Certificate" -ErrorAction SilentlyContinue - - if ($currentThumbprint) { - Write-Information "[VERBOSE] Current certificate thumbprint: $currentThumbprint" - } else { - Write-Information "[VERBOSE] No existing certificate thumbprint found" - } - - # Set new thumbprint - Set-ItemProperty -Path $RegistryPath -Name "Certificate" -Value $NewThumbprint -ErrorAction Stop - - # Verify the change - $verifyThumbprint = Get-ItemPropertyValue -Path $RegistryPath -Name "Certificate" -ErrorAction Stop - - if ($verifyThumbprint -eq $NewThumbprint) { - Write-Information "Registry updated successfully" - } else { - throw "Registry update verification failed. Expected: $NewThumbprint, Got: $verifyThumbprint" - } - } - catch { - Write-Error "Failed to update registry at '$RegistryPath': $_" - throw $_ - } - - # ============================================================ - # STEP 3: Get SQL Server Service Information - # ============================================================ - Write-Information "Retrieving SQL Server service information..." - - try { - $serviceName = Get-SqlServiceName -InstanceName $InstanceName - $serviceInfo = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction Stop - $SqlServiceUser = $serviceInfo.StartName - - if (-not $SqlServiceUser) { - throw "Unable to retrieve service account for SQL Server instance: $InstanceName" - } - - # Normalize service account name for ACL operations - if ($SqlServiceUser -eq "LocalSystem") { - $SqlServiceUser = "NT AUTHORITY\SYSTEM" - Write-Information "[VERBOSE] Normalized LocalSystem to: $SqlServiceUser" - } - elseif ($SqlServiceUser -match "^NT Service\\") { - # NT Service accounts are already in correct format - Write-Information "[VERBOSE] Using NT Service account: $SqlServiceUser" - } - elseif ($SqlServiceUser.StartsWith(".\")) { - # Local account - convert to machine\user format - $SqlServiceUser = "$env:COMPUTERNAME$($SqlServiceUser.Substring(1))" - Write-Information "[VERBOSE] Normalized local account to: $SqlServiceUser" - } - - Write-Information "[VERBOSE] Service name: $serviceName" - Write-Information "SQL Server service account: $SqlServiceUser" - } - catch { - Write-Error "Failed to retrieve SQL Server service information: $_" - throw $_ - } - - # ============================================================ - # STEP 4: Locate Certificate and Private Key - # ============================================================ - Write-Information "Locating certificate and private key..." - - try { - # Get the certificate from the LocalMachine\My store - $Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $NewThumbprint } - - if (-not $Cert) { - throw "Certificate with thumbprint $NewThumbprint not found in LocalMachine\My store" - } - - Write-Information "[VERBOSE] Certificate found: $($Cert.Subject)" - - if (-not $Cert.HasPrivateKey) { - throw "Certificate does not have a private key" - } - - # Detect private key location (CNG vs CAPI) - $privKeyPath = $null - $privKey = $null - $keyStorageType = $null - - # Try CNG first (modern certificates) - try { - $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Cert) - - if ($rsaKey -is [System.Security.Cryptography.RSACng]) { - $privKey = $rsaKey.Key.UniqueName - $keyStorageType = "CNG" - Write-Information "[VERBOSE] Certificate uses CNG key storage" - Write-Information "[VERBOSE] CNG key unique name: $privKey" - - # CNG keys can be in multiple locations - check them all - $possiblePaths = @( - "$($env:ProgramData)\Microsoft\Crypto\Keys\$privKey", - "$($env:ProgramData)\Microsoft\Crypto\SystemKeys\$privKey", - "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\$privKey" - ) - - Write-Information "[VERBOSE] Searching for CNG private key in known locations..." - foreach ($path in $possiblePaths) { - Write-Information "[VERBOSE] Checking: $path" - if (Test-Path $path) { - $privKeyPath = Get-Item $path -ErrorAction Stop - Write-Information "[VERBOSE] Found CNG private key at: $path" - break - } - } - - # If not found in standard locations, search more broadly - if (-not $privKeyPath) { - Write-Information "[VERBOSE] Key not found in standard locations. Searching all Crypto directories..." - - $searchPaths = @( - "$($env:ProgramData)\Microsoft\Crypto\Keys", - "$($env:ProgramData)\Microsoft\Crypto\SystemKeys", - "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys" - ) - - foreach ($searchPath in $searchPaths) { - if (Test-Path $searchPath) { - $found = Get-ChildItem -Path $searchPath -Filter "*$privKey*" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - $privKeyPath = $found - Write-Information "[VERBOSE] Found CNG private key at: $($privKeyPath.FullName)" - break - } - } - } - } - - if ($privKeyPath) { - Write-Information "Certificate uses CNG (Cryptography Next Generation) key storage" - } - } - } - catch { - Write-Information "[VERBOSE] CNG key detection failed or not applicable: $_" - Write-Information "[VERBOSE] Exception type: $($_.Exception.GetType().FullName)" - } - - # Fallback to CAPI/CSP (legacy certificates) - if (-not $privKey -or -not $privKeyPath) { - Write-Information "[VERBOSE] Attempting CAPI/CSP key detection..." - - try { - if ($Cert.PrivateKey -and $Cert.PrivateKey.CspKeyContainerInfo) { - $privKey = $Cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName - $keyPath = "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\" - $keyStorageType = "CAPI/CSP" - - Write-Information "[VERBOSE] CAPI/CSP key unique name: $privKey" - Write-Information "[VERBOSE] Expected path: $keyPath$privKey" - - $privKeyPath = Get-Item "$keyPath\$privKey" -ErrorAction Stop - Write-Information "Certificate uses CAPI/CSP (legacy) key storage" - } - } - catch { - Write-Information "[VERBOSE] CAPI/CSP key detection failed: $_" - } - } - - if (-not $privKey) { - throw "Unable to locate private key for certificate. The certificate may not have an accessible private key." - } - - if (-not $privKeyPath) { - # Last resort: try to find the key file by searching - Write-Warning "Private key file not found in expected locations. Attempting comprehensive search..." - - $allCryptoPaths = @( - "$($env:ProgramData)\Microsoft\Crypto\Keys", - "$($env:ProgramData)\Microsoft\Crypto\SystemKeys", - "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys" - ) - - foreach ($searchPath in $allCryptoPaths) { - if (Test-Path $searchPath) { - Write-Information "[VERBOSE] Searching in: $searchPath" - $found = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like "*$privKey*" -or $_.Name -eq $privKey } | - Select-Object -First 1 - - if ($found) { - $privKeyPath = $found - Write-Information "Private key found at: $($privKeyPath.FullName)" - break - } - } - } - - if (-not $privKeyPath) { - throw "Unable to locate private key file for certificate with thumbprint $NewThumbprint. Key name: $privKey" - } - } - - Write-Information "Private key located at: $($privKeyPath.FullName)" - Write-Information "[VERBOSE] Key storage type: $keyStorageType" - Write-Information "[VERBOSE] Key file size: $($privKeyPath.Length) bytes" - - # Verify we can read the key file - try { - $acl = Get-Acl -Path $privKeyPath.FullName -ErrorAction Stop - Write-Information "[VERBOSE] Successfully accessed private key file ACL" - } - catch { - Write-Warning "Could not read ACL from private key file: $_" - } - } - catch { - Write-Error "Failed to locate certificate or private key: $_" - throw $_ - } - - # ============================================================ - # STEP 5: Set ACL Permissions on Private Key - # ============================================================ - Write-Information "Setting ACL permissions on private key for SQL service account..." - - try { - $aclSet = $false - $aclMethod = $null - - # Attempt 1: Try Set-Acl (works in most local scenarios and some SSH sessions) - try { - Write-Information "[VERBOSE] Attempting ACL update using Set-Acl method..." - - $Acl = Get-Acl -Path $privKeyPath -ErrorAction Stop - $Ar = New-Object System.Security.AccessControl.FileSystemAccessRule( - $SqlServiceUser, - "Read", - "Allow" - ) - $Acl.SetAccessRule($Ar) - Set-Acl -Path $privKeyPath.FullName -AclObject $Acl -ErrorAction Stop - - # Verify the ACL was actually set - $verifyAcl = Get-Acl -Path $privKeyPath - $hasPermission = $verifyAcl.Access | Where-Object { - ($_.IdentityReference.Value -eq $SqlServiceUser -or - $_.IdentityReference.Value -like "*$SqlServiceUser*") -and - $_.FileSystemRights -match "Read" - } - - if ($hasPermission) { - Write-Information "ACL updated successfully using Set-Acl method" - $aclSet = $true - $aclMethod = "Set-Acl" - } else { - Write-Warning "Set-Acl completed but verification failed. Permissions may not be set correctly." - } - } - catch { - Write-Warning "Set-Acl method failed: $_" - Write-Information "[VERBOSE] Error details: $($_.Exception.Message)" - } - - # Attempt 2: Use icacls (more reliable in SSH sessions) - if (-not $aclSet) { - Write-Information "[VERBOSE] Attempting ACL update using icacls method..." - - try { - # Execute icacls to grant Read permissions - $icaclsResult = & icacls.exe $privKeyPath.FullName /grant "${SqlServiceUser}:(R)" 2>&1 - - if ($LASTEXITCODE -eq 0) { - Write-Information "[VERBOSE] icacls command executed successfully" - - # Verify with icacls - $verifyResult = & icacls.exe $privKeyPath.FullName 2>&1 - - if ($verifyResult -match [regex]::Escape($SqlServiceUser)) { - Write-Information "ACL updated successfully using icacls method" - $aclSet = $true - $aclMethod = "icacls" - } else { - Write-Warning "icacls completed but verification failed" - } - } else { - Write-Warning "icacls failed with exit code $LASTEXITCODE" - Write-Information "[VERBOSE] icacls output: $icaclsResult" - } - } - catch { - Write-Warning "icacls method failed: $_" - } - } - - # Attempt 3: Use Scheduled Task (fallback for restricted SSH sessions) - if (-not $aclSet) { - Write-Warning "Standard ACL methods failed. Attempting scheduled task method (elevated privileges)..." - - try { - # Create a temporary script to set the ACL - $tempScriptPath = Join-Path $env:TEMP "SetCertACL_$((Get-Date).Ticks).ps1" - - $scriptContent = @" -try { - `$privKeyPath = '$($privKeyPath.FullName)' - `$SqlServiceUser = '$SqlServiceUser' - - # Try icacls first - `$result = & icacls.exe `$privKeyPath /grant "`${SqlServiceUser}:(R)" 2>&1 - - if (`$LASTEXITCODE -eq 0) { - Set-Content -Path '$env:TEMP\acl_success.txt' -Value "Success via icacls" - } else { - # Fallback to Set-Acl - `$Acl = Get-Acl -Path `$privKeyPath - `$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule( - `$SqlServiceUser, - 'Read', - 'Allow' - ) - `$Acl.SetAccessRule(`$Ar) - Set-Acl -Path `$privKeyPath -AclObject `$Acl - Set-Content -Path '$env:TEMP\acl_success.txt' -Value "Success via Set-Acl" - } -} catch { - Set-Content -Path '$env:TEMP\acl_error.txt' -Value `$_.Exception.Message -} -"@ - - Set-Content -Path $tempScriptPath -Value $scriptContent - Write-Information "[VERBOSE] Created temporary script: $tempScriptPath" - - # Create and register the scheduled task - $taskName = "SetCertACL_$((Get-Date).Ticks)" - $action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$tempScriptPath`"" - $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2) - $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable - - Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null - Write-Information "[VERBOSE] Scheduled task registered: $taskName" - - # Wait for task to complete - Write-Information "[VERBOSE] Waiting for scheduled task to complete..." - Start-Sleep -Seconds 5 - - # Check results - if (Test-Path "$env:TEMP\acl_success.txt") { - $successMessage = Get-Content "$env:TEMP\acl_success.txt" -Raw - Write-Information "ACL updated successfully using scheduled task method ($successMessage)" - Remove-Item "$env:TEMP\acl_success.txt" -Force -ErrorAction SilentlyContinue - $aclSet = $true - $aclMethod = "Scheduled Task" - } - elseif (Test-Path "$env:TEMP\acl_error.txt") { - $errorMessage = Get-Content "$env:TEMP\acl_error.txt" -Raw - Remove-Item "$env:TEMP\acl_error.txt" -Force -ErrorAction SilentlyContinue - throw "Scheduled task failed: $errorMessage" - } - else { - throw "Scheduled task did not complete or produce expected output" - } - - # Cleanup - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - Remove-Item $tempScriptPath -Force -ErrorAction SilentlyContinue - } - catch { - Write-Warning "Scheduled task method failed: $_" - } - } - - # Final check - if (-not $aclSet) { - throw "Failed to set ACL permissions using all available methods (Set-Acl, icacls, Scheduled Task)" - } - - Write-Information "ACL permissions configured successfully using: $aclMethod" - - } - catch { - Write-Error "Failed to update ACL on the private key: $_" - Write-Error "SQL Server may not be able to use this certificate without proper permissions." - throw $_ - } - - # ============================================================ - # STEP 6: Restart SQL Server Service (Optional) - # ============================================================ - if ($RestartService) { - Write-Information "Restarting SQL Server service..." - - try { - # Get current service status - $service = Get-Service -Name $serviceName -ErrorAction Stop - $originalStatus = $service.Status - - Write-Information "[VERBOSE] Current service status: $originalStatus" - - # Stop the service if running - if ($originalStatus -eq 'Running') { - Write-Information "Stopping SQL Server service: $serviceName" - Stop-Service -Name $serviceName -Force -ErrorAction Stop - - # Wait for service to stop (with timeout) - $stopTimeout = 60 - $elapsed = 0 - - while ((Get-Service -Name $serviceName).Status -ne 'Stopped' -and $elapsed -lt $stopTimeout) { - Start-Sleep -Seconds 2 - $elapsed += 2 - Write-Information "[VERBOSE] Waiting for service to stop... ($elapsed seconds)" - } - - if ((Get-Service -Name $serviceName).Status -ne 'Stopped') { - throw "Service did not stop within $stopTimeout seconds" - } - - Write-Information "SQL Server service stopped successfully" - } - - # Start the service - Write-Information "Starting SQL Server service: $serviceName" - Start-Service -Name $serviceName -ErrorAction Stop - - # Wait for service to start (with timeout) - $startTimeout = 90 - $elapsed = 0 - - while ((Get-Service -Name $serviceName).Status -ne 'Running' -and $elapsed -lt $startTimeout) { - Start-Sleep -Seconds 2 - $elapsed += 2 - Write-Information "[VERBOSE] Waiting for service to start... ($elapsed seconds)" - } - - $finalStatus = (Get-Service -Name $serviceName).Status - - if ($finalStatus -eq 'Running') { - Write-Information "SQL Server service restarted successfully" - } else { - throw "Service did not start within $startTimeout seconds. Current status: $finalStatus" - } - } - catch { - Write-Error "Failed to restart SQL Server service: $_" - Write-Warning "Certificate binding completed but service restart failed." - Write-Warning "Please restart SQL Server manually to apply the certificate binding." - Write-Warning "You can restart using: Restart-Service -Name '$serviceName' -Force" - - # Don't throw here - the certificate binding succeeded - # Just warn the user to restart manually - } - } else { - Write-Information "Service restart skipped. You must restart SQL Server for the certificate binding to take effect." - Write-Information "To restart: Restart-Service -Name '$serviceName' -Force" - } - - # ============================================================ - # SUCCESS - # ============================================================ - Write-Information "==========================================" - Write-Information "Certificate binding completed successfully!" - Write-Information "Instance: $InstanceName" - Write-Information "Certificate: $NewThumbprint" - Write-Information "Service Account: $SqlServiceUser" - Write-Information "Key Storage: $keyStorageType" - Write-Information "ACL Method: $aclMethod" - - if ($RestartService) { - Write-Information "Service Status: Restarted" - } else { - Write-Information "Service Status: Restart Required" - } - Write-Information "==========================================" - - return $true - } - catch { - Write-Error "Certificate binding failed for instance $InstanceName" - Write-Error "Error: $_" - Write-Information "[VERBOSE] Stack trace: $($_.ScriptStackTrace)" - return $false - } -} - -# Migrated -function Unbind-KFSqlCertificate { - param ( - [string]$SQLInstanceNames, # Comma-separated list of SQL instances - [switch]$RestartService # Restart SQL Server after unbinding - ) - - $unBindingSuccess = $true # Track success/failure - - try { - - $instances = $SQLInstanceNames -split ',' | ForEach-Object { $_.Trim() } - - foreach ($instance in $instances) { - try { - # Resolve full instance name from registry - $fullInstance = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance - $regPath = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$fullInstance\MSSQLServer\SuperSocketNetLib" - - # Get current thumbprint - $certificateThumbprint = Get-ItemPropertyValue -Path $regPath -Name "Certificate" -ErrorAction SilentlyContinue - - if ($certificateThumbprint) { - Write-Information "Unbinding certificate from SQL Server instance: $instance (Thumbprint: $certificateThumbprint)" - - # Instead of deleting, set to an empty string to prevent SQL startup issues - Set-ItemProperty -Path $regPath -Name "Certificate" -Value "" - - Write-Information "Successfully unbound certificate from SQL Server instance: $instance." - } else { - Write-Warning "No certificate bound to SQL Server instance: $instance." - } - - # Restart service if required - if ($RestartService) { - $serviceName = Get-SqlServiceName -InstanceName $instance - Write-Information "Restarting SQL Server service: $serviceName..." - Restart-Service -Name $serviceName -Force - Write-Information "SQL Server service restarted successfully." - } - } - catch { - Write-Error "Failed to unbind certificate from instance: $instance. Error: $_" - $unBindingSuccess = $false - } - } - } - catch { - Write-Error "An unexpected error occurred: $($_.Exception.Message)" - return $false - } - - return $unBindingSuccess - -# Example usage: -# Bind-CertificateToSqlInstance -Thumbprint "123ABC456DEF" -SqlInstanceName "MSSQLSERVER" -} - -# Migrated -function Get-SqlServiceName { - param ( - [string]$InstanceName - ) - if ($InstanceName -eq "MSSQLSERVER") { - return "MSSQLSERVER" # Default instance - } else { - return "MSSQL`$$InstanceName" # Named instance (escape $ for PowerShell strings) - } -} - -# Migrated -function Get-SQLServiceUser { - param ( - [Parameter(Mandatory = $true)] - [string]$SQLServiceName - ) - - # Use Get-CimInstance instead of Get-WmiObject - $serviceUser = (Get-CimInstance -ClassName Win32_Service -Filter "Name='$SQLServiceName'").StartName - - if ($serviceUser) { - return $serviceUser - } else { - Write-Error "SQL Server instance '$SQLInstanceName' not found or no service user associated." - return $null - } - -# Example usage: -# Get-SQLServiceUser -SQLInstanceName "MSSQLSERVER" -} -##### - -##### ReEnrollment (ODKG) functions -# Migrated -function New-CSREnrollment { - param ( - [string]$SubjectText, - [string]$ProviderName = "Microsoft Strong Cryptographic Provider", - [string]$KeyType, - [string]$KeyLength, - [string]$SAN - ) - - if ([string]::IsNullOrWhiteSpace($ProviderName)) { - $ProviderName = "Microsoft Strong Cryptographic Provider" - } - - # Validate the Crypto Service Provider - Validate-CryptoProvider -ProviderName $ProviderName - - # Parse Subject for any escaped commas - $parsedSubject = Parse-DNSubject $SubjectText - - # Build the SAN entries if provided - $sanContent = "" - if ($SAN) { - $sanEntries = $SAN -split "&" - $sanDirectives = $sanEntries | ForEach-Object { "_continue_ = `"$($_)&`"" } - $sanContent = @" -[Extensions] -2.5.29.17 = `"{text}`" -$($sanDirectives -join "`n") -"@ - } - - # Generate INF file content for the CSR - $infContent = @" -[Version] -Signature=`"$`Windows NT$`" - -[NewRequest] -Subject = "$parsedSubject" -ProviderName = "$ProviderName" -MachineKeySet = True -HashAlgorithm = SHA256 -KeyAlgorithm = $KeyType -KeyLength = $KeyLength -KeySpec = 0 - -$sanContent -"@ - - Write-Information "[VERBOSE] INF Contents: $infContent" - - # Path to temporary INF file - $infFile = [System.IO.Path]::GetTempFileName() + ".inf" - $csrOutputFile = [System.IO.Path]::GetTempFileName() + ".csr" - - Set-Content -Path $infFile -Value $infContent - Write-Information "Generated INF file at: $infFile" - - try { - # Run certreq to generate CSR - $certReqCommand = "certreq -new -q `"$infFile`" `"$csrOutputFile`"" - Write-Information "Running certreq: $certReqCommand" - - # Capture the output and errors - $certReqOutput = & certreq -new -q $infFile $csrOutputFile 2>&1 - - # Check the exit code of the command - if ($LASTEXITCODE -ne 0) { - $errMsg = "Certreq failed with exit code $LASTEXITCODE. Output: $certReqOutput" - throw $errMsg - } - - # If successful, proceed - Write-Information "Certreq completed successfully." - - # Read CSR file - if (Test-Path $csrOutputFile) { - $csrContent = Get-Content -Path $csrOutputFile -Raw - Write-Information "CSR successfully created at: $csrOutputFile" - return $csrContent - } else { - throw "Failed to create CSR file." - } - } catch { - Write-Error $_ - } finally { - # Clean up temporary files - if (Test-Path $infFile) { - Remove-Item -Path $infFile -Force - Write-Information "Deleted temporary INF file." - } - - if (Test-Path $csrOutputFile) { - Remove-Item -Path $csrOutputFile -Force - Write-Information "Deleted temporary CSR file." - } - } -} - -function Import-SignedCertificate { - param ( - [Parameter(Mandatory = $true)] - [byte[]]$RawData, # RawData from the certificate - - [Parameter(Mandatory = $true)] - [string]$StoreName # Store to which the certificate should be imported - ) - - try { - # Step 1: Convert raw certificate data to Base64 string with line breaks - Write-Information "[VERBOSE] Converting raw certificate data to Base64 string." - $csrData = [System.Convert]::ToBase64String($RawData, [System.Base64FormattingOptions]::InsertLineBreaks) - - # Step 2: Create PEM-formatted certificate content - Write-Information "[VERBOSE] Creating PEM-formatted certificate content." - $certContent = @( - "-----BEGIN CERTIFICATE-----" - $csrData - "-----END CERTIFICATE-----" - ) -join "`n" - - # Step 3: Create a temporary file for the certificate - Write-Information "[VERBOSE] Creating a temporary file for the certificate." - $cerFilename = [System.IO.Path]::GetTempFileName() - Set-Content -Path $cerFilename -Value $certContent -Force - Write-Information "[VERBOSE] Temporary certificate file created at: $cerFilename" - - # Step 4: Import the certificate into the specified store - Write-Information "[VERBOSE] Importing the certificate to the store: Cert:\LocalMachine\$StoreName" - Set-Location -Path "Cert:\LocalMachine\$StoreName" - - $importResult = Import-Certificate -FilePath $cerFilename - if ($importResult) { - Write-Information "[VERBOSE] Certificate successfully imported to Cert:\LocalMachine\$StoreName." - } else { - throw "Certificate import failed." - } - - # Step 5: Cleanup temporary file - if (Test-Path $cerFilename) { - Remove-Item -Path $cerFilename -Force - Write-Information "[VERBOSE] Temporary file deleted: $cerFilename" - } - - # Step 6: Return the imported certificate's thumbprint - return $importResult.Thumbprint - - } catch { - Write-Error "An error occurred during the certificate export and import process: $_" - } -} -##### - -# Shared Functions -# Function to get SAN (Subject Alternative Names) from a certificate -# Migrated -function Get-KFSAN($cert) { - $san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" } - if ($san) { - return ($san.Format(1) -split ", " -join "; ") - } - return $null -} - -#Function to verify if the given CSP is found on the computer -# Migrated -function Test-CryptoServiceProvider { - param( - [Parameter(Mandatory = $true)] - [string]$CSPName - ) - - try { - Validate-CryptoProvider -ProviderName $CSPName -Verbose:$false - return $true - } - catch { - return $false - } -} - -# Function that takes an x509 certificate object and returns the csp -# Migrated -function Get-CertificateCSP { - param( - [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert - ) - - # Helper: extract KSP/provider name from a CNG key object - function Get-CngProviderName { - param($key) - try { - # RSACng / ECDsaCng expose a .Key property (CngKey) - if ($key.PSObject.Properties['Key']) { - $cngKey = $key.Key - if ($cngKey -and $cngKey.Provider -and $cngKey.Provider.Provider) { - return [string]$cngKey.Provider.Provider - } - } - } - catch { - Write-Information "[VERBOSE] CNG provider lookup failed: $($_.Exception.Message)" - } - return $null - } - - try { - if (-not $cert.HasPrivateKey) { - return "No private key" - } - - # ── 1. Legacy CryptoAPI path (RSACryptoServiceProvider) ────────────── - $privateKey = $cert.PrivateKey - if ($privateKey -and $privateKey.CspKeyContainerInfo) { - $providerName = $privateKey.CspKeyContainerInfo.ProviderName - if ($providerName) { - return [string]$providerName - } - } - - # ── 2. CNG RSA (RSACng) ─────────────────────────────────────────────── - try { - $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) - if ($rsaKey) { - $providerName = Get-CngProviderName $rsaKey - if ($providerName) { return $providerName } - } - } - catch { - Write-Information "[VERBOSE] RSA CNG detection failed: $($_.Exception.Message)" - } - - # ── 3. ECC / ECDsa (ECDsaCng) ───────────────────────────────────────── - # ECC keys always use CNG (KSPs), never legacy CSPs - try { - $ecKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($cert) - if ($ecKey) { - $providerName = Get-CngProviderName $ecKey - if ($providerName) { return $providerName } - - Write-Information "[VERBOSE] ECC key detected but no resolvable provider name (type: $($ecKey.GetType().Name))" - return "" - } - } - catch { - Write-Information "[VERBOSE] ECDsa CNG detection failed: $($_.Exception.Message)" - } - - # ── 4. DSA (bonus) ──────────────────────────────────────────────────── - try { - $dsaKey = [System.Security.Cryptography.X509Certificates.DSACertificateExtensions]::GetDSAPrivateKey($cert) - if ($dsaKey) { - $providerName = Get-CngProviderName $dsaKey - if ($providerName) { return $providerName } - - Write-Information "[VERBOSE] DSA key detected but no resolvable provider name (type: $($dsaKey.GetType().Name))" - return "" - } - } - catch { - Write-Information "[VERBOSE] DSA CNG detection failed: $($_.Exception.Message)" - } - - Write-Information "[VERBOSE] No supported key type detected; provider name could not be determined" - return "" - } - catch { - Write-Warning "Error retrieving CSP for certificate '$($cert.Subject)': $($_.Exception.Message)" - return "" - } -} - -# Migrated -function Get-CryptoProviders { - # Retrieves the list of available Crypto Service Providers using certutil - try { - Write-Information "[VERBOSE] Retrieving Crypto Service Providers using certutil..." - $certUtilOutput = certutil -csplist - - # Parse the output to extract CSP names - $cspInfoList = @() - foreach ($line in $certUtilOutput) { - if ($line -match "Provider Name:") { - $cspName = ($line -split ":")[1].Trim() - $cspInfoList += $cspName - } - } - - if ($cspInfoList.Count -eq 0) { - throw "No Crypto Service Providers were found. Ensure certutil is functioning properly." - } - - Write-Information "[VERBOSE] Retrieved the following CSPs:" - $cspInfoList | ForEach-Object { Write-Information "[VERBOSE] $_" } - - return $cspInfoList - } catch { - throw "Failed to retrieve Crypto Service Providers: $_" - } -} - -# Migrated -function Validate-CryptoProvider { - param ( - [Parameter(Mandatory)] - [string]$ProviderName - ) - Write-Information "[VERBOSE] Validating CSP: $ProviderName" - - $availableProviders = Get-CryptoProviders - - if (-not ($availableProviders | Where-Object { $_.Trim().ToLowerInvariant() -eq $ProviderName.Trim().ToLowerInvariant() })) { - - throw "Crypto Service Provider '$ProviderName' is either invalid or not found on this system." - } - - Write-Information "[VERBOSE] Crypto Service Provider '$ProviderName' is valid." -} - -# Migrated -function Convert-DNSubject { - <# - .SYNOPSIS - Parses a Distinguished Name (DN) subject string and properly quotes RDN values containing escaped commas. - - .DESCRIPTION - This function takes a DN subject string and parses the Relative Distinguished Name (RDN) components, - adding proper quotes around values that contain escaped commas and escaping quotes for use in - PowerShell here-strings. Only RDN values with escaped commas get quoted. - - .PARAMETER Subject - The DN subject string to parse (e.g., "CN=Keyfactor,O=Keyfactor\, Inc") - - .EXAMPLE - Parse-DNSubject -Subject "CN=Keyfactor,O=Keyfactor\, Inc" - Returns: CN=Keyfactor,O=""Keyfactor, Inc"" - - .EXAMPLE - Parse-DNSubject -Subject "CN=Test User,O=Company\, LLC,OU=IT Department\, Security" - Returns: CN=Test User,O=""Company, LLC"",OU=""IT Department, Security"" - #> - - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string]$Subject - ) - - # Initialize variables - $parsedComponents = @() - $currentComponent = "" - $i = 0 - - # Convert string to character array for easier parsing - $chars = $Subject.ToCharArray() - - while ($i -lt $chars.Length) { - $char = $chars[$i] - - # Check if we hit a comma - if ($char -eq ',') { - # Look back to see if it's escaped - $isEscaped = $false - if ($i -gt 0 -and $chars[$i-1] -eq '\') { - $isEscaped = $true - } - - if ($isEscaped) { - # This is an escaped comma, add it to current component - $currentComponent += $char - } else { - # This is a separator comma, finish current component - if ($currentComponent.Trim() -ne "") { - $parsedComponents += $currentComponent.Trim() - $currentComponent = "" - } - } - } else { - # Regular character, add to current component - $currentComponent += $char - } - - $i++ - } - - # Add the last component - if ($currentComponent.Trim() -ne "") { - $parsedComponents += $currentComponent.Trim() - } - - # Process each component to add quotes where needed - $processedComponents = @() - - foreach ($component in $parsedComponents) { - # Split on first equals sign to get attribute and value - $equalIndex = $component.IndexOf('=') - if ($equalIndex -gt 0) { - $attribute = $component.Substring(0, $equalIndex).Trim() - $value = $component.Substring($equalIndex + 1).Trim() - - # Clean up escaped commas first - $cleanValue = $value -replace '\\,', ',' - - # Check if original value had escaped commas (needs quotes) - if ($value -match '\\,') { - # This RDN value had escaped commas, so wrap in doubled quotes and escape quotes - $escapedValue = $cleanValue -replace '"', '""' - $processedComponents += "$attribute=`"`"$escapedValue`"`"" - } else { - # No escaped commas, keep as simple value but escape any existing quotes - $escapedValue = $cleanValue -replace '"', '""' - $processedComponents += "$attribute=$escapedValue" - } - } else { - # Invalid component format, keep as is - $processedComponents += $component - } - } - - # Join components back together (no outer quotes needed since it goes in PowerShell string) - $subjectString = ($processedComponents -join ',') - return $subjectString -} - -#### Functions to test SSL flags -# Migrated -function Get-ValidSslFlagsForSystem { - <# - .SYNOPSIS - Gets the valid SSL flag bits for the current Windows Server version - #> - [CmdletBinding()] - param() - - $build = [System.Environment]::OSVersion.Version.Build - - # Return array of valid flag values based on Windows Server version - if ($build -ge 20348) { - # Windows Server 2022+ (IIS 10.0.20348+) - Write-Information "[VERBOSE] Detected Windows Server 2022 or later (Build: $build)" - return @(1, 4, 8, 16, 32, 64) # Include unknowns for testing - } - elseif ($build -ge 17763) { - # Windows Server 2019 (IIS 10.0.17763) - Write-Information "[VERBOSE] Detected Windows Server 2019 (Build: $build)" - return @(1, 4, 8) - } - elseif ($build -ge 14393) { - # Windows Server 2016 (IIS 10.0) - Write-Information "[VERBOSE] Detected Windows Server 2016 (Build: $build)" - return @(1, 4) - } - else { - # Windows Server 2012 R2 and earlier (IIS 8.5) - Write-Information "[VERBOSE] Detected Windows Server 2012 R2 or earlier (Build: $build)" - return @(1, 2) - } -} - -# Migrated -function Test-ValidSslFlags { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [int]$Flags, - - [Parameter(Mandatory = $false)] - [switch]$ThrowOnError - ) - - $build = [System.Environment]::OSVersion.Version.Build - $validBits = Get-ValidSslFlagsForSystem - - # Calculate valid bitmask - $validMask = 0 - foreach ($bit in $validBits) { - $validMask = $validMask -bor $bit - } - - # Check for unknown/unsupported bits - $unknownBits = $Flags -band (-bnot $validMask) - if ($unknownBits -ne 0) { - $errorMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) contains unsupported bits " + - "for this Windows Server version (Build: $build): $unknownBits (0x$($unknownBits.ToString('X'))). " + - "Supported flags: $($validBits -join ', ')" - - if ($ThrowOnError) { - throw $errorMsg - } - else { - return [PSCustomObject]@{ - IsValid = $false - ErrorCode = 400 - Message = $errorMsg - WindowsBuild = $build - ValidFlags = $validBits - InvalidBits = $unknownBits - } - } - } - - # Check for known invalid combinations - $hasSni = ($Flags -band 1) -ne 0 - $hasCentralCert = ($Flags -band 2) -ne 0 - - if ($hasCentralCert -and -not $hasSni) { - $errorMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is invalid: " + - "CentralCertStore (0x2) requires SNI (0x1) to be enabled." - - if ($ThrowOnError) { - throw $errorMsg - } - else { - return [PSCustomObject]@{ - IsValid = $false - ErrorCode = 400 - Message = $errorMsg - WindowsBuild = $build - ValidFlags = $validBits - InvalidBits = 0 - } - } - } - - # Validation passed - $successMsg = "SslFlags value $Flags (0x$($Flags.ToString('X'))) is valid for this system (Build: $build)." - - if ($ThrowOnError) { - Write-Information "[VERBOSE] $successMsg" - return $true - } - else { - return [PSCustomObject]@{ - IsValid = $true - ErrorCode = 0 - Message = $successMsg - WindowsBuild = $build - ValidFlags = $validBits - InvalidBits = 0 - } - } -} - -# Not used -function Get-SslFlagDescription { - <# - .SYNOPSIS - Returns a human-readable description of SSL flags - - .PARAMETER Flags - The SSL flags value to describe - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [int]$Flags - ) - - $descriptions = @() - - if (($Flags -band 1) -ne 0) { $descriptions += "SNI (Server Name Indication)" } - if (($Flags -band 2) -ne 0) { $descriptions += "CentralCertStore (Centralized Certificate Store)" } - if (($Flags -band 4) -ne 0) { $descriptions += "DisableHTTP2 (Disable HTTP/2)" } - if (($Flags -band 16) -ne 0) { $descriptions += "DisableQUIC (Disable QUIC/HTTP3)" } - if (($Flags -band 32) -ne 0) { $descriptions += "DisableTLS13 (Disable TLS 1.3 over TCP)" } - if (($Flags -band 64) -ne 0) { $descriptions += "DisableLegacyTLS (Disable Legacy TLS)" } - - if ($descriptions.Count -eq 0) { - return "None (0x0)" - } - - return ($descriptions -join ", ") -} -#### - -# Note: Removed Test-IISBindingConflict function - we now mimic IIS behavior -# IIS replaces exact matches and allows multiple hostnames (SNI) on same IP:Port -# Migrated -function Get-IISManagementInfo { - [CmdletBinding()] - [OutputType([hashtable])] - param ( - [Parameter(Mandatory = $true)] - [string]$SiteName - ) - - $hasIISDrive = Ensure-IISDrive - Write-Information "[VERBOSE] IIS Drive available: $hasIISDrive" - - if ($hasIISDrive) { - $null = Import-Module WebAdministration - $sitePath = "IIS:\Sites\$SiteName" - - if (-not (Test-Path $sitePath)) { - $errorMessage = "Site '$SiteName' not found in IIS drive" - Write-Error $errorMessage - return @{ - Success = $false - UseIISDrive = $true - Result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } - } - } - - return @{ - Success = $true - UseIISDrive = $true - Result = $null - } - } - else { - # ServerManager fallback - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager - $site = $iis.Sites[$SiteName] - - if ($null -eq $site) { - $errorMessage = "Site '$SiteName' not found in ServerManager" - Write-Error $errorMessage - return @{ - Success = $false - UseIISDrive = $false - Result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } - } - } - - return @{ - Success = $true - UseIISDrive = $false - Result = $null - } - } -} - -# Migrated -function Ensure-IISDrive { - [CmdletBinding()] - param () - - # Try to import the WebAdministration module if not already loaded - if (-not (Get-Module -Name WebAdministration)) { - try { - $null = Import-Module WebAdministration -ErrorAction Stop - } - catch { - Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." - return $false - } - } - - # Check if IIS drive is available - if (-not (Get-PSDrive -Name 'IIS' -ErrorAction SilentlyContinue)) { - Write-Warning "IIS:\ drive not available. Ensure IIS is installed and the WebAdministration module is imported." - return $false - } - - return $true -} \ No newline at end of file diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index 0583059f..d8b94155 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -65,9 +65,6 @@ Always - - Always - From a9cd8332a266242d95a5e1ab6f93ad4c6b320e24 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 6 May 2026 12:31:12 -0500 Subject: [PATCH 10/53] changed way LocalHost and JEA are configured. --- IISU/PSHelper.cs | 9 +++++++ .../PSHelperConfigTests.cs | 26 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 1337b4cd..c788732a 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -142,6 +142,15 @@ public void Initialize() _logger.LogTrace($"Script file located here: {scriptFileLocation}"); + if (isLocalMachine && useJea) + { + throw new Exception( + $"Ambiguous configuration: the store target is set to the local machine but JEA endpoint '{jeaEndpoint}' is also configured. " + + "JEA requires a remote WinRM connection and cannot be used with a local machine store. " + + "To resolve: either clear the JEA Endpoint Name to use a direct local connection, " + + "or replace 'LocalMachine'/'localhost' with the server's hostname or IP address to connect via JEA over WinRM."); + } + if (!isLocalMachine) { InitializeRemoteSession(); diff --git a/WindowsCertStore.UnitTests/PSHelperConfigTests.cs b/WindowsCertStore.UnitTests/PSHelperConfigTests.cs index c2798700..ec07f0d8 100644 --- a/WindowsCertStore.UnitTests/PSHelperConfigTests.cs +++ b/WindowsCertStore.UnitTests/PSHelperConfigTests.cs @@ -23,7 +23,6 @@ public void PSHelper_ParameterizedConstructor_DoesNotThrow(string jeaEndpoint) } [Theory] - [InlineData(null, "testdir", null)] [InlineData("C:\\nonexistent\\path", "PowerShell", null)] public void FindScriptsDirectory_ReturnsNullForMissingDirectory(string baseDir, string folderName, string expected) { @@ -39,5 +38,30 @@ public void FindScriptsDirectory_FindsPowerShellFolderFromBaseDirectory() Assert.NotNull(result); Assert.True(Directory.Exists(result), $"Expected directory to exist: {result}"); } + + [Theory] + [InlineData("localhost")] + [InlineData("LocalMachine")] + [InlineData("localmachine")] + public void PSHelper_Initialize_LocalMachineWithJEA_ThrowsAmbiguousConfigException(string localMachineName) + { + var ps = new PSHelper("http", "5985", false, localMachineName, "user", "pass", jeaEndpoint: "keyfactor.wincert"); + var ex = Record.Exception(() => ps.Initialize()); + Assert.NotNull(ex); + Assert.Contains("Ambiguous configuration", ex.Message); + Assert.Contains("keyfactor.wincert", ex.Message); + } + + [Fact] + public void PSHelper_Initialize_LocalMachineWithoutJEA_DoesNotThrowAmbiguousConfig() + { + // No JEA endpoint — local machine path should proceed past the guard clause + // (it will fail later trying to create an out-of-process runspace in a test context, + // but the ambiguous-config exception specifically should NOT be thrown) + var ps = new PSHelper("http", "5985", false, "localhost", "user", "pass", jeaEndpoint: ""); + var ex = Record.Exception(() => ps.Initialize()); + Assert.True(ex == null || !ex.Message.Contains("Ambiguous configuration"), + $"Should not throw ambiguous config error, but got: {ex?.Message}"); + } } } From 0c432a9d51aa5c413fc2f0b45b4f867c478249e9 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 6 May 2026 12:57:39 -0500 Subject: [PATCH 11/53] Fixed ODKG missing file error --- IISU/ClientPSCertStoreReEnrollment.cs | 27 ++++----- .../Keyfactor.WinCert.Common.psm1 | 2 + .../Import-KeyfactorSignedCertificate.ps1 | 59 +++++++++++++++++++ .../Keyfactor.WinCert.Common.psrc | 1 + 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 diff --git a/IISU/ClientPSCertStoreReEnrollment.cs b/IISU/ClientPSCertStoreReEnrollment.cs index da9adc2b..bdcaa309 100644 --- a/IISU/ClientPSCertStoreReEnrollment.cs +++ b/IISU/ClientPSCertStoreReEnrollment.cs @@ -90,12 +90,11 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit string protocol = jobProperties.WinRmProtocol; string port = jobProperties.WinRmPort; bool includePortInSPN = jobProperties.SpnPortFlag; + string jeaEndpoint = jobProperties?.JEAEndpointName ?? ""; string clientMachineName = config.CertificateStoreDetails.ClientMachine; string storePath = config.CertificateStoreDetails.StorePath; - //_psHelper = new(protocol, port, includePortInSPN, clientMachineName, serverUserName, serverPassword); - - _psHelper = new(protocol, port, includePortInSPN, clientMachineName, serverUserName, serverPassword); + _psHelper = new(protocol, port, includePortInSPN, clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint); _psHelper.Initialize(); using (_psHelper) @@ -160,25 +159,25 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit { case "Success": psResult = OrchestratorJobStatusJobResult.Success; - _logger.LogDebug($"PowerShell function New-KFIISSiteBinding returned successfully with Code: {code}, on Step: {step}"); + _logger.LogDebug($"PowerShell function New-KeyfactorIISSiteBinding returned successfully with Code: {code}, on Step: {step}"); break; case "Skipped": psResult = OrchestratorJobStatusJobResult.Failure; - failureMessage = ($"PowerShell function New-KFIISSiteBinding failed on step: {step} - message:\n {errorMessage}"); + failureMessage = ($"PowerShell function New-KeyfactorIISSiteBinding failed on step: {step} - message:\n {errorMessage}"); _logger.LogDebug(failureMessage); break; case "Warning": psResult = OrchestratorJobStatusJobResult.Warning; - _logger.LogDebug($"PowerShell function New-KFIISSiteBinding returned with a Warning on step: {step} with code: {code} - message: {message}"); + _logger.LogDebug($"PowerShell function New-KeyfactorIISSiteBinding returned with a Warning on step: {step} with code: {code} - message: {message}"); break; case "Error": psResult = OrchestratorJobStatusJobResult.Failure; - failureMessage = ($"PowerShell function New-KFIISSiteBinding failed on step: {step} with code: {code} - message: {errorMessage}"); + failureMessage = ($"PowerShell function New-KeyfactorIISSiteBinding failed on step: {step} with code: {code} - message: {errorMessage}"); _logger.LogDebug(failureMessage); break; default: psResult = OrchestratorJobStatusJobResult.Unknown; - _logger.LogWarning("Unknown status returned from New-KFIISSiteBinding: " + status); + _logger.LogWarning("Unknown status returned from New-KeyfactorIISSiteBinding: " + status); break; } } @@ -294,9 +293,9 @@ private string CreateCSR(string subjectText, string providerName, string keyType { "keyLength", keySize }, { "SAN", SAN } }; - _logger.LogInformation("Attempting to execute PS function (New-CsrEnrollment)"); - _results = _psHelper.ExecutePowerShell("New-CsrEnrollment", parameters); - _logger.LogInformation("Returned from executing PS function (New-CsrEnrollment)"); + _logger.LogInformation("Attempting to execute PS function (New-KeyfactorODKGEnrollment)"); + _results = _psHelper.ExecutePowerShell("New-KeyfactorODKGEnrollment", parameters); + _logger.LogInformation("Returned from executing PS function (New-KeyfactorODKGEnrollment)"); // This should return the CSR that was generated if (_results == null || _results.Count == 0) @@ -356,9 +355,9 @@ private string ImportCertificate(byte[] certificateRawData, string storeName) { "storeName", storeName } }; - _logger.LogTrace("Attempting to execute PS function (Import-SignedCertificate)"); - _results = _psHelper.ExecutePowerShell("Import-SignedCertificate", parameters); - _logger.LogTrace("Returned from executing PS function (Import-SignedCertificate)"); + _logger.LogTrace("Attempting to execute PS function (Import-KeyfactorSignedCertificate)"); + _results = _psHelper.ExecutePowerShell("Import-KeyfactorSignedCertificate", parameters); + _logger.LogTrace("Returned from executing PS function (Import-KeyfactorSignedCertificate)"); // This should return the CSR that was generated if (_results != null && _results.Count > 0) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 index 8b80e7f8..35e7e6b3 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 @@ -18,6 +18,7 @@ . "$PSScriptRoot\Public\Add-KeyfactorCertificate.ps1" . "$PSScriptRoot\Public\Remove-KeyfactorCertificate.ps1" . "$PSScriptRoot\Public\New-KeyfactorODKGEnrollment.ps1" +. "$PSScriptRoot\Public\Import-KeyfactorSignedCertificate.ps1" # Export only public functions for non-JEA use. # In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. @@ -28,6 +29,7 @@ Export-ModuleMember -Function @( 'Add-KeyfactorCertificate', 'Remove-KeyfactorCertificate', 'New-KeyfactorODKGEnrollment', + 'Import-KeyfactorSignedCertificate', # Shared certificate inspection utilities — exported so other modules (e.g. IIS) can call them 'Get-CertificateCSP', 'Get-CertificateSAN' diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 new file mode 100644 index 00000000..6627be40 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 @@ -0,0 +1,59 @@ +function Import-KeyfactorSignedCertificate { + param ( + [Parameter(Mandatory = $true)] + [byte[]]$RawData, + + [Parameter(Mandatory = $true)] + [string]$StoreName + ) + + $tempCertFile = $null + try { + Write-Information "Entering Import-KeyfactorSignedCertificate" + + # Extract thumbprint from the raw certificate bytes + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($RawData) + $thumbprint = $cert.Thumbprint + if (-not $thumbprint) { + throw "Failed to get thumbprint from the signed certificate." + } + Write-Information "Certificate thumbprint: $thumbprint" + + # Write to temp .cer file so certreq can process it + $tempCertFile = [System.IO.Path]::GetTempFileName() + ".cer" + [System.IO.File]::WriteAllBytes($tempCertFile, $RawData) + + # certreq -accept links the signed certificate to the matching pending private key + # that was created when New-KeyfactorODKGEnrollment ran certreq -new. + Write-Information "Running certreq -accept to complete enrollment" + $output = & certreq -accept $tempCertFile 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "certreq -accept failed (exit code $LASTEXITCODE). Output: $output" + } + Write-Information "certreq -accept completed successfully." + + # certreq -accept installs into the 'My' (Personal) store. + # If the target store is different, also register the cert there. + $normalizedStore = $StoreName.Trim() + if ($normalizedStore -ine 'My' -and $normalizedStore -ine 'Personal') { + Write-Information "Adding certificate to store '$normalizedStore'" + $addOutput = & certutil -f -addstore $normalizedStore $tempCertFile 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "certutil -addstore for store '$normalizedStore' returned exit code $LASTEXITCODE. Output: $addOutput" + } else { + Write-Information "Certificate added to store '$normalizedStore'." + } + } + + return $thumbprint + } + catch { + Write-Error "An error occurred in Import-KeyfactorSignedCertificate: $_" + return $null + } + finally { + if ($tempCertFile -and (Test-Path $tempCertFile)) { + Remove-Item $tempCertFile -Force + } + } +} diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc index 4450cee3..68ddc77c 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc @@ -37,6 +37,7 @@ 'Remove-KeyfactorCertificate', 'New-KeyfactorResult', 'New-KeyfactorODKGEnrollment', + 'Import-KeyfactorSignedCertificate', 'Get-CertificateCSP', 'Get-CertificateSAN' ) From eca4c198dbc841b971e4db3e85b896937e3126a2 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 7 May 2026 14:38:31 -0700 Subject: [PATCH 12/53] Updated KF Import of Signed Certificates PowerShell Script --- .../Import-KeyfactorSignedCertificate.ps1 | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 index 6627be40..8bc9ce5e 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1 @@ -1,59 +1,52 @@ function Import-KeyfactorSignedCertificate { param ( [Parameter(Mandatory = $true)] - [byte[]]$RawData, + [byte[]]$RawData, # RawData from the certificate [Parameter(Mandatory = $true)] - [string]$StoreName + [string]$StoreName # Store to which the certificate should be imported ) - $tempCertFile = $null try { - Write-Information "Entering Import-KeyfactorSignedCertificate" + # Step 1: Convert raw certificate data to Base64 string with line breaks + Write-Verbose "Converting raw certificate data to Base64 string." + $csrData = [System.Convert]::ToBase64String($RawData, [System.Base64FormattingOptions]::InsertLineBreaks) - # Extract thumbprint from the raw certificate bytes - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($RawData) - $thumbprint = $cert.Thumbprint - if (-not $thumbprint) { - throw "Failed to get thumbprint from the signed certificate." - } - Write-Information "Certificate thumbprint: $thumbprint" - - # Write to temp .cer file so certreq can process it - $tempCertFile = [System.IO.Path]::GetTempFileName() + ".cer" - [System.IO.File]::WriteAllBytes($tempCertFile, $RawData) - - # certreq -accept links the signed certificate to the matching pending private key - # that was created when New-KeyfactorODKGEnrollment ran certreq -new. - Write-Information "Running certreq -accept to complete enrollment" - $output = & certreq -accept $tempCertFile 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "certreq -accept failed (exit code $LASTEXITCODE). Output: $output" - } - Write-Information "certreq -accept completed successfully." - - # certreq -accept installs into the 'My' (Personal) store. - # If the target store is different, also register the cert there. - $normalizedStore = $StoreName.Trim() - if ($normalizedStore -ine 'My' -and $normalizedStore -ine 'Personal') { - Write-Information "Adding certificate to store '$normalizedStore'" - $addOutput = & certutil -f -addstore $normalizedStore $tempCertFile 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning "certutil -addstore for store '$normalizedStore' returned exit code $LASTEXITCODE. Output: $addOutput" - } else { - Write-Information "Certificate added to store '$normalizedStore'." - } + # Step 2: Create PEM-formatted certificate content + Write-Verbose "Creating PEM-formatted certificate content." + $certContent = @( + "-----BEGIN CERTIFICATE-----" + $csrData + "-----END CERTIFICATE-----" + ) -join "`n" + + # Step 3: Create a temporary file for the certificate + Write-Verbose "Creating a temporary file for the certificate." + $cerFilename = [System.IO.Path]::GetTempFileName() + Set-Content -Path $cerFilename -Value $certContent -Force + Write-Verbose "Temporary certificate file created at: $cerFilename" + + # Step 4: Import the certificate into the specified store + Write-Verbose "Importing the certificate to the store: Cert:\LocalMachine\$StoreName" + Set-Location -Path "Cert:\LocalMachine\$StoreName" + + $importResult = Import-Certificate -FilePath $cerFilename + if ($importResult) { + Write-Verbose "Certificate successfully imported to Cert:\LocalMachine\$StoreName." + } else { + throw "Certificate import failed." } - return $thumbprint - } - catch { - Write-Error "An error occurred in Import-KeyfactorSignedCertificate: $_" - return $null - } - finally { - if ($tempCertFile -and (Test-Path $tempCertFile)) { - Remove-Item $tempCertFile -Force + # Step 5: Cleanup temporary file + if (Test-Path $cerFilename) { + Remove-Item -Path $cerFilename -Force + Write-Verbose "Temporary file deleted: $cerFilename" } + + # Step 6: Return the imported certificate's thumbprint + return $importResult.Thumbprint + + } catch { + Write-Error "An error occurred during the certificate export and import process: $_" } -} +} \ No newline at end of file From f9bd02b8a774e3ff841c2abc582dddc3beb8e532 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 11 May 2026 09:28:59 -0500 Subject: [PATCH 13/53] Fixed missing PowerShell script not being found. --- .../Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 | 6 +++--- .../Remove-KeyfactorIISCertificateIfUnused.ps1 | 0 .../RoleCapabilities/Keyfactor.WinCert.IIS.psrc | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) rename IISU/PowerShell/Keyfactor.WinCert.IIS/{Private => Public}/Remove-KeyfactorIISCertificateIfUnused.ps1 (100%) diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 index 5aba30fc..6778330b 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 @@ -22,17 +22,17 @@ if (-not (Get-Command 'New-KeyfactorResult' -ErrorAction SilentlyContinue)) { . "$PSScriptRoot\Private\Test-ValidSslFlags.ps1" . "$PSScriptRoot\Private\Add-IISBindingWithSSL.ps1" . "$PSScriptRoot\Private\Get-IISManagementInfo.ps1" -. "$PSScriptRoot\Private\Remove-KeyfactorIISCertificateIfUnused.ps1" - # Public functions . "$PSScriptRoot\Public\Get-KeyfactorIISBoundCertificates.ps1" . "$PSScriptRoot\Public\Remove-KeyfactorIISSiteBinding.ps1" . "$PSScriptRoot\Public\New-KeyfactorIISSiteBinding.ps1" +. "$PSScriptRoot\Public\Remove-KeyfactorIISCertificateIfUnused.ps1" # Export only public functions for non-JEA use. # In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. Export-ModuleMember -Function @( 'Get-KeyfactorIISBoundCertificates', 'New-KeyfactorIISSiteBinding', - 'Remove-KeyfactorIISSiteBinding' + 'Remove-KeyfactorIISSiteBinding', + 'Remove-KeyfactorIISCertificateIfUnused' ) diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 similarity index 100% rename from IISU/PowerShell/Keyfactor.WinCert.IIS/Private/Remove-KeyfactorIISCertificateIfUnused.ps1 rename to IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc b/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc index fce84114..7c488c56 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc @@ -27,7 +27,8 @@ VisibleFunctions = @( 'Get-KeyfactorIISBoundCertificates', 'New-KeyfactorIISSiteBinding', - 'Remove-KeyfactorIISSiteBinding' + 'Remove-KeyfactorIISSiteBinding', + 'Remove-KeyfactorIISCertificateIfUnused' ) VisibleCmdlets = @( From df9ddcf769d010bc00e997b661fc1f2828c11790 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 11 May 2026 10:01:05 -0500 Subject: [PATCH 14/53] Fix missing path --- .../Public/Remove-KeyfactorIISCertificateIfUnused.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 index d134f4ca..9a673fd9 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 @@ -49,7 +49,7 @@ function Remove-KeyfactorIISCertificateIfUnused { return } - Remove-Item -Path $cert.PSPath -Force + Remove-Item -Path "Cert:\LocalMachine\$StoreName\$normalizedThumbprint" -Force Write-Information "[VERBOSE] Certificate $normalizedThumbprint removed from Cert:\LocalMachine\$StoreName" } catch { From 8ad923c344ab80dbb009ed7674cfec734d88adbf Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 11 May 2026 16:56:09 -0500 Subject: [PATCH 15/53] Updated documentation to include JEA information. --- docsource/content.md | 371 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 370 insertions(+), 1 deletion(-) diff --git a/docsource/content.md b/docsource/content.md index 87a03395..820979c1 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -58,7 +58,366 @@ dnf install openssh-clients openssl
Using the WinCert Extension on Windows servers: -1. When orchestrating management of external (and potentially local) certificate stores, the WinCert Orchestrator Extension makes use of WinRM to connect to external certificate store servers. The security context used is the user id entered in the Keyfactor Command certificate store. Make sure that WinRM is set up on the orchestrated server and that the WinRM port (by convention, 5585 for HTTP and 5586 for HTTPS) is part of the certificate store path when setting up your certificate stores jobs. If running as an agent, managing local certificate stores, local commands are run under the security context of the user account running the Keyfactor Universal Orchestrator Service. +1. When orchestrating management of external (and potentially local) certificate stores, the WinCert Orchestrator Extension makes use of WinRM to connect to external certificate store servers. The security context used is the user id entered in the Keyfactor Command certificate store. Make sure that WinRM is set up on the orchestrated server and that the WinRM port (by convention, 5985 for HTTP and 5986 for HTTPS) is part of the certificate store path when setting up your certificate stores jobs. If running as an agent, managing local certificate stores, local commands are run under the security context of the user account running the Keyfactor Universal Orchestrator Service. + +2. **JEA (Just Enough Administration) Support** — As a more secure alternative to granting the orchestrator service account full local administrator rights, the WinCert extension supports connecting via a JEA-enabled WinRM session endpoint. When JEA is configured, the orchestrator connects to a named PowerShell session configuration on the target server. Within that session, only the specific Keyfactor certificate management functions are exposed — no general PowerShell commands, no file system access, and no administrative cmdlets are available to the connecting account. This dramatically reduces the attack surface on managed servers and allows you to follow the principle of least privilege. JEA is configured on a per-certificate-store basis by entering the JEA endpoint name in the **JEA Endpoint Name** parameter when creating or editing a certificate store in Keyfactor Command. Refer to the **Just Enough Administration (JEA) Setup and Configuration** section below for complete step-by-step setup instructions. + +3. **Important:** JEA cannot be used when the certificate store is configured to access the local machine directly (i.e., when the Client Machine value contains `|LocalMachine` or is set to `localhost`/`LocalMachine`). JEA requires an actual WinRM network connection to the target server. If a JEA Endpoint Name is configured and the store is also set to LocalMachine, the job will fail immediately with an ambiguous configuration error. To manage a local machine's certificates using JEA, set the Client Machine to the server's actual hostname or IP address and configure the JEA endpoint normally. + +
+ +
+Just Enough Administration (JEA) Setup and Configuration: + +### What is JEA? + +Just Enough Administration (JEA) is a PowerShell security technology built into Windows that allows administrators to create constrained, audited remote PowerShell sessions. Instead of granting a service account full administrative access to a server, JEA lets you define exactly which PowerShell functions, cmdlets, and external commands are permitted within a remote session. The connecting account runs commands in that restricted environment — it cannot browse the file system, run arbitrary scripts, or invoke any command that has not been explicitly permitted. + +JEA operates through two types of configuration files: + +- **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. + +- **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. + +When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `ConstrainedLanguage` PowerShell session backed by pre-installed, fully-trusted module code. The orchestrator can invoke Keyfactor certificate management functions, but nothing else. Every command executed in the session is recorded to a transcript file for audit purposes. + +--- + +### Why Use JEA with the WinCert Extension? + +The default WinRM connection model requires the orchestrator service account to have local administrator rights on every managed server. While functional, this violates the principle of least privilege and creates a broad attack surface — if the service account credentials were ever compromised, an attacker would have administrative access to every managed server. JEA addresses this by: + +- **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. +- **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. +- **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. +- **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. + +--- + +### How JEA Works with the WinCert Extension + +When the **JEA Endpoint Name** field is populated on a certificate store, the orchestrator changes its connection behavior: + +1. It connects to the target server via WinRM using the configured credentials, but specifies the named JEA session configuration (`-ConfigurationName keyfactor.wincert`) instead of opening a standard administrative session. +2. The JEA session loads the pre-installed Keyfactor PowerShell modules from the target server's system module path (`C:\Program Files\WindowsPowerShell\Modules\`). Because these modules are installed in a trusted location, they run as fully trusted code and can use .NET APIs freely. +3. The orchestrator does **not** inject script content into the session. Instead, it calls the pre-loaded module functions by name, passing parameters. This is different from the standard WinRM mode, which loads scripts at session start. +4. A pre-flight check verifies that the Keyfactor modules are installed and accessible before any job runs. If the modules are not found, the job fails immediately with an actionable error message. +5. All commands executed during the session are written to a transcript in `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. + +--- + +### Prerequisites + +Before configuring JEA on a target server, ensure the following: + +- **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). +- **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. +- **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. +- **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. + +--- + +### Keyfactor PowerShell Module Overview + +The WinCert extension ships three PowerShell modules. Each module contains a `RoleCapabilities` subfolder with a `.psrc` file that defines which functions are visible in a JEA session. + +| Module | Store Types Supported | Purpose | +|---|---|---| +| `Keyfactor.WinCert.Common` | WinCert, WinIIS, WinSQL | Certificate inventory, add, remove, and re-enrollment (CSR generation and signed cert import). Required for all store types. | +| `Keyfactor.WinCert.IIS` | WinIIS | IIS site binding management (get, create, remove bindings). | +| `Keyfactor.WinCert.SQL` | WinSQL | SQL Server certificate binding management (get, bind, unbind). | + +Install only the modules needed for the store types you manage on that server. For example, a server that only hosts IIS certificates needs `Keyfactor.WinCert.Common` and `Keyfactor.WinCert.IIS`. + +--- + +### Step-by-Step Setup Guide + +#### Step 1: Locate the JEA Configuration Files + +After deploying the Keyfactor Universal Orchestrator with the WinCert extension, navigate to the extension's output directory. You will find a `PowerShell` folder containing: + +``` +PowerShell\ + Keyfactor.WinCert.Common\ ← Module: common certificate operations + Keyfactor.WinCert.IIS\ ← Module: IIS binding management + Keyfactor.WinCert.SQL\ ← Module: SQL Server binding management + Build\ + KeyfactorWinCert.pssc ← JEA Session Configuration file +``` + +Copy this entire `PowerShell` folder to the target server (or to a network share accessible from the target server) to perform the setup steps below. + +--- + +#### Step 2: Install the Keyfactor PowerShell Modules on the Target Server + +On the **target server**, open an elevated PowerShell prompt (Run as Administrator) and run the following commands. Adjust the source path (`$sourcePath`) to wherever you placed the `PowerShell` folder in Step 1. + +```powershell +# Set the source path to where you copied the PowerShell folder +$sourcePath = 'C:\Temp\PowerShell' + +# System module path — modules installed here are treated as fully trusted by PowerShell +$moduleBase = 'C:\Program Files\WindowsPowerShell\Modules' + +# Always install the Common module — required for all store types +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.Common" ` + -Destination "$moduleBase\Keyfactor.WinCert.Common" ` + -Recurse -Force + +# Install the IIS module if this server hosts IIS certificate stores (WinIIS) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.IIS" ` + -Destination "$moduleBase\Keyfactor.WinCert.IIS" ` + -Recurse -Force + +# Install the SQL module if this server hosts SQL Server certificate stores (WinSQL) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.SQL" ` + -Destination "$moduleBase\Keyfactor.WinCert.SQL" ` + -Recurse -Force +``` + +> **Important:** Modules **must** be installed under `C:\Program Files\WindowsPowerShell\Modules\` (or another path listed in the system `$env:PSModulePath`). Modules installed outside of a trusted path will not run as fully trusted code inside a `ConstrainedLanguage` JEA session, and calls to .NET APIs will fail. + +Verify that the modules installed correctly by running: + +```powershell +Get-Module -ListAvailable | Where-Object { $_.Name -like 'Keyfactor.*' } +``` + +You should see entries for each module you installed. + +--- + +#### Step 3: Create the Audit Transcript Directory + +JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. + +```powershell +New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force +``` + +Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. + +--- + +#### Step 4: Review and Customize the Session Configuration File + +Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working location on the target server (e.g., `C:\Temp\KeyfactorWinCert.pssc`) and open it in a text editor. The key settings to review and customize are: + +**Run-As Account (choose one):** + +The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: + +- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. + + ```powershell + RunAsVirtualAccount = $true + ``` + +- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. + + ```powershell + # Comment out RunAsVirtualAccount and uncomment this line: + GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' + ``` + + To create a gMSA (run on a domain controller or with AD PowerShell module): + ```powershell + # Create the gMSA in Active Directory + New-ADServiceAccount -Name 'KeyfactorJEA' ` + -DNSHostName 'keyfactorjea.yourdomain.com' ` + -PrincipalsAllowedToRetrieveManagedPassword 'KeyfactorServers$' + + # On the target server, install the gMSA + Install-ADServiceAccount -Identity 'KeyfactorJEA$' + + # Verify the gMSA can log on + Test-ADServiceAccount -Identity 'KeyfactorJEA$' + ``` + +**Role Definitions (who is allowed to connect):** + +The `RoleDefinitions` section maps connecting users or groups to JEA role capabilities. Replace `BUILTIN\Administrators` with the specific AD group or local group whose members should be allowed to connect via JEA. Using a dedicated AD group is strongly recommended for production environments. + +```powershell +RoleDefinitions = @{ + # Replace with the AD group that contains your Keyfactor orchestrator service accounts: + 'DOMAIN\KeyfactorOrchestrators' = @{ + RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' + } +} +``` + +Only list the `RoleCapabilities` whose corresponding modules are installed on this server. The available combinations are: + +| Store Types on This Server | RoleCapabilities to List | +|---|---| +| WinCert only | `'Keyfactor.WinCert.Common'` | +| WinIIS only or WinCert + WinIIS | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS'` | +| WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | +| WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | + +--- + +#### Step 5: Register the JEA Session Configuration + +On the **target server**, in an elevated PowerShell prompt, register the session configuration. This creates the named WinRM endpoint that the Keyfactor orchestrator will connect to. + +```powershell +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +# WinRM must be restarted for the new endpoint to become active +Restart-Service WinRM +``` + +> **Note:** `Restart-Service WinRM` will briefly interrupt all active WinRM connections on the server. Schedule this during a maintenance window if other services depend on WinRM. + +The name `keyfactor.wincert` is the endpoint name you will enter into the **JEA Endpoint Name** field in Keyfactor Command. You may use a different name if desired — just ensure it matches exactly when configuring the certificate store. + +--- + +#### Step 6: Verify the Registration + +Confirm the endpoint is registered and its configuration is correct: + +```powershell +# List all registered session configurations +Get-PSSessionConfiguration | Where-Object { $_.Name -eq 'keyfactor.wincert' } +``` + +You should see output showing the configuration name, PSVersion, and the path to the `.pssc` file. + +For a more thorough validation, connect to the JEA endpoint from a remote machine and verify that the Keyfactor functions are available: + +```powershell +# Connect to the JEA endpoint (run this from the Keyfactor orchestrator server or any machine with network access) +$cred = Get-Credential # Enter the orchestrator service account credentials +$s = New-PSSession -ComputerName '' ` + -Port 5985 ` + -ConfigurationName 'keyfactor.wincert' ` + -Credential $cred + +# List all commands available in the JEA session (should be limited to Keyfactor functions only) +Invoke-Command -Session $s -ScriptBlock { Get-Command } + +# Test a certificate inventory call (WinCert) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } + +# Test IIS inventory (if Keyfactor.WinCert.IIS is installed on the target) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorIISBoundCertificates } + +# Clean up the test session +Remove-PSSession $s +``` + +The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. + +--- + +#### Step 7: Configure the Certificate Store in Keyfactor Command + +Once the JEA endpoint is registered and verified on the target server, configure the certificate store in Keyfactor Command: + +1. Navigate to the certificate store you wish to manage via JEA (or create a new one). +2. In the store's **Custom Parameters** (also called **Store Properties**), locate the **JEA Endpoint Name** field. +3. Enter the name of the JEA session configuration you registered — for example, `keyfactor.wincert`. +4. Ensure the **Client Machine** is set to the target server's hostname or IP address. **Do not** use `localhost`, `LocalMachine`, or the `|LocalMachine` suffix — JEA requires a real WinRM network connection. +5. The **Server Username** and **Server Password** fields should contain the credentials of the account that is permitted to connect to the JEA endpoint (i.e., a member of the group specified in `RoleDefinitions` in the `.pssc` file). +6. Save the certificate store. + +When a job runs against this store, the orchestrator will automatically use the JEA endpoint instead of a standard WinRM administrative session. + +--- + +### Updating the JEA Configuration + +If you need to change the session configuration — for example, to add a new module or change the run-as account — update the `.pssc` file and re-register it: + +```powershell +# Re-register with the -Force flag to overwrite the existing registration +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +Restart-Service WinRM +``` + +If you update one of the Keyfactor modules (e.g., after upgrading the WinCert extension), repeat Step 2 to copy the new module files to the target server. No re-registration of the session configuration is necessary for module-only updates — the next JEA session will load the updated module automatically. + +--- + +### Removing the JEA Configuration + +To remove the JEA endpoint from a server: + +```powershell +Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' +Restart-Service WinRM +``` + +After removing the endpoint, any certificate stores in Keyfactor Command that reference the JEA endpoint name will fail. Update those stores to either clear the **JEA Endpoint Name** field (to revert to standard WinRM) or point to a different JEA endpoint. + +--- + +### Troubleshooting + +**"JEA endpoint is reachable but Keyfactor modules are not installed"** + +The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). + +**"The term 'Get-KeyfactorCertificates' is not recognized..."** + +The function is not visible in the JEA session. Verify that: +- The module containing that function is installed on the target server (Step 2). +- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). +- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). +- The session configuration was re-registered and WinRM was restarted after any changes. + +**"Connecting user is not authorized to connect to this configuration"** + +The account used in the certificate store credentials is not a member of any group listed in `RoleDefinitions`. Add the account (or a group containing it) to the `RoleDefinitions` section in the `.pssc`, re-register the configuration, and restart WinRM. + +**"Access is denied" or "WinRM cannot complete the operation"** + +This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: +- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). +- The WinRM firewall rule allows connections from the orchestrator server's IP. +- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. + +**"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** + +A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. + +**Reviewing JEA Transcripts** + +All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. + +```powershell +# List recent transcript files +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | Sort-Object LastWriteTime -Descending | Select-Object -First 10 + +# View the most recent transcript +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content +``` + +--- + +### Important Notes and Limitations + +- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. +- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. +- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. +- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. +- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. +- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection.
@@ -68,6 +427,16 @@ Please consult with your company's system administrator for more information on PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. +**JEA Module Requirements:** When using JEA (Just Enough Administration) to connect to a target server, the Keyfactor PowerShell modules must be pre-installed on each target server under `C:\Program Files\WindowsPowerShell\Modules\`. These modules are included in the WinCert extension deployment package inside the `PowerShell` folder. The modules that must be installed depend on which store types are managed on that server: + +| Module | Required For | +|---|---| +| `Keyfactor.WinCert.Common` | All store types — must always be installed | +| `Keyfactor.WinCert.IIS` | WinIIS stores | +| `Keyfactor.WinCert.SQL` | WinSQL stores | + +In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatically loads these modules from its own deployment at runtime — no pre-installation on the target server is required. JEA mode is the only mode that requires the modules to be pre-installed on the target server. See the **Just Enough Administration (JEA) Setup and Configuration** section for complete installation and setup instructions. + ### Security and Permission Considerations From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. From 4094f15ff6a3205456f90523413d84659849b232 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 11 May 2026 21:57:40 +0000 Subject: [PATCH 16/53] Update generated docs --- README.md | 120 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 2ea905e7..11a190c9 100644 --- a/README.md +++ b/README.md @@ -113,57 +113,103 @@ dnf install openssh-clients openssl
Using the WinCert Extension on Windows servers: -1. When orchestrating management of external (and potentially local) certificate stores, the WinCert Orchestrator Extension makes use of WinRM to connect to external certificate store servers. The security context used is the user id entered in the Keyfactor Command certificate store. Make sure that WinRM is set up on the orchestrated server and that the WinRM port (by convention, 5585 for HTTP and 5586 for HTTPS) is part of the certificate store path when setting up your certificate stores jobs. If running as an agent, managing local certificate stores, local commands are run under the security context of the user account running the Keyfactor Universal Orchestrator Service. +1. When orchestrating management of external (and potentially local) certificate stores, the WinCert Orchestrator Extension makes use of WinRM to connect to external certificate store servers. The security context used is the user id entered in the Keyfactor Command certificate store. Make sure that WinRM is set up on the orchestrated server and that the WinRM port (by convention, 5985 for HTTP and 5986 for HTTPS) is part of the certificate store path when setting up your certificate stores jobs. If running as an agent, managing local certificate stores, local commands are run under the security context of the user account running the Keyfactor Universal Orchestrator Service. + +2. **JEA (Just Enough Administration) Support** — As a more secure alternative to granting the orchestrator service account full local administrator rights, the WinCert extension supports connecting via a JEA-enabled WinRM session endpoint. When JEA is configured, the orchestrator connects to a named PowerShell session configuration on the target server. Within that session, only the specific Keyfactor certificate management functions are exposed — no general PowerShell commands, no file system access, and no administrative cmdlets are available to the connecting account. This dramatically reduces the attack surface on managed servers and allows you to follow the principle of least privilege. JEA is configured on a per-certificate-store basis by entering the JEA endpoint name in the **JEA Endpoint Name** parameter when creating or editing a certificate store in Keyfactor Command. Refer to the **Just Enough Administration (JEA) Setup and Configuration** section below for complete step-by-step setup instructions. + +3. **Important:** JEA cannot be used when the certificate store is configured to access the local machine directly (i.e., when the Client Machine value contains `|LocalMachine` or is set to `localhost`/`LocalMachine`). JEA requires an actual WinRM network connection to the target server. If a JEA Endpoint Name is configured and the store is also set to LocalMachine, the job will fail immediately with an ambiguous configuration error. To manage a local machine's certificates using JEA, set the Client Machine to the server's actual hostname or IP address and configure the JEA endpoint normally.
-Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. +
+Just Enough Administration (JEA) Setup and Configuration: + +### What is JEA? + +Just Enough Administration (JEA) is a PowerShell security technology built into Windows that allows administrators to create constrained, audited remote PowerShell sessions. Instead of granting a service account full administrative access to a server, JEA lets you define exactly which PowerShell functions, cmdlets, and external commands are permitted within a remote session. The connecting account runs commands in that restricted environment — it cannot browse the file system, run arbitrary scripts, or invoke any command that has not been explicitly permitted. + +JEA operates through two types of configuration files: + +- **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. + +- **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. + +When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `ConstrainedLanguage` PowerShell session backed by pre-installed, fully-trusted module code. The orchestrator can invoke Keyfactor certificate management functions, but nothing else. Every command executed in the session is recorded to a transcript file for audit purposes. + +--- + +### Why Use JEA with the WinCert Extension? + +The default WinRM connection model requires the orchestrator service account to have local administrator rights on every managed server. While functional, this violates the principle of least privilege and creates a broad attack surface — if the service account credentials were ever compromised, an attacker would have administrative access to every managed server. JEA addresses this by: + +- **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. +- **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. +- **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. +- **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. + +--- + +### How JEA Works with the WinCert Extension -### PowerShell Requirements -PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). -In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. +When the **JEA Endpoint Name** field is populated on a certificate store, the orchestrator changes its connection behavior: -### Security and Permission Considerations +1. It connects to the target server via WinRM using the configured credentials, but specifies the named JEA session configuration (`-ConfigurationName keyfactor.wincert`) instead of opening a standard administrative session. +2. The JEA session loads the pre-installed Keyfactor PowerShell modules from the target server's system module path (`C:\Program Files\WindowsPowerShell\Modules\`). Because these modules are installed in a trusted location, they run as fully trusted code and can use .NET APIs freely. +3. The orchestrator does **not** inject script content into the session. Instead, it calls the pre-loaded module functions by name, passing parameters. This is different from the standard WinRM mode, which loads scripts at session start. +4. A pre-flight check verifies that the Keyfactor modules are installed and accessible before any job runs. If the modules are not found, the job fails immediately with an actionable error message. +5. All commands executed during the session are written to a transcript in `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. -From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. - -For customers wishing to use something other than the local administrator account, the following information may be helpful: - -* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). - -* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) - -* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. - -* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. - -* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - - Create and delete temporary files. - - Execute certreq commands. - - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. +--- -### Using Crypto Service Providers (CSP) -When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. +### Prerequisites -The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: +Before configuring JEA on a target server, ensure the following: - certutil -csplist +- **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). +- **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. +- **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. +- **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. -When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. +--- -When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. +### Keyfactor PowerShell Module Overview -Each CSP only supports certain key types and algorithms. +The WinCert extension ships three PowerShell modules. Each module contains a `RoleCapabilities` subfolder with a `.psrc` file that defines which functions are visible in a JEA session. -Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: -|CSP Name|Supports RSA?|Supports ECC?| +| Module | Store Types Supported | Purpose | |---|---|---| -|Microsoft RSA SChannel Cryptographic Provider |✅|❌| -|Microsoft Software Key Storage Provider |✅|✅| -|Microsoft Enhanced Cryptographic Provider |✅|❌| +| `Keyfactor.WinCert.Common` | WinCert, WinIIS, WinSQL | Certificate inventory, add, remove, and re-enrollment (CSR generation and signed cert import). Required for all store types. | +| `Keyfactor.WinCert.IIS` | WinIIS | IIS site binding management (get, create, remove bindings). | +| `Keyfactor.WinCert.SQL` | WinSQL | SQL Server certificate binding management (get, bind, unbind). | + +Install only the modules needed for the store types you manage on that server. For example, a server that only hosts IIS certificates needs `Keyfactor.WinCert.Common` and `Keyfactor.WinCert.IIS`. + +--- + +### Step-by-Step Setup Guide + +#### Step 1: Locate the JEA Configuration Files + +After deploying the Keyfactor Universal Orchestrator with the WinCert extension, navigate to the extension's output directory. You will find a `PowerShell` folder containing: + +``` +PowerShell\ + Keyfactor.WinCert.Common\ ← Module: common certificate operations + Keyfactor.WinCert.IIS\ ← Module: IIS binding management + Keyfactor.WinCert.SQL\ ← Module: SQL Server binding management + Build\ + KeyfactorWinCert.pssc ← JEA Session Configuration file +``` + +Copy this entire `PowerShell` folder to the target server (or to a network share accessible from the target server) to perform the setup steps below. + +--- + +#### Step 2: Install the Keyfactor PowerShell Modules on the Target Server + +On the **target server**, open an elevated PowerShell prompt (Run as Administrator) and run the following commands. Adjust the source path (`$sourcePath`) to wherever you placed the `PowerShell` folder in Step 1. + +```powershell ## Certificate Store Types From 9e102ea728644febada15254a1d1e27be8164432 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 12 May 2026 09:12:01 -0700 Subject: [PATCH 17/53] chore(ci): Update build workflow to v5 --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 8ec5771e..b0c525a6 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} # Only required for doctool generated screenshots command_hostname: ${{ vars.COMMAND_HOSTNAME }} # Only required for doctool generated screenshots From 2158f3c08da0aecb16f874e28ddc4f14c13a80b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 May 2026 16:12:47 +0000 Subject: [PATCH 18/53] docs: auto-generate README and documentation [skip ci] --- README.md | 402 +++++++----------- .../IISU-advanced-store-type-dialog.svg | 67 +++ .../images/IISU-basic-store-type-dialog.svg | 84 ++++ .../IISU-custom-field-ServerUseSsl-dialog.svg | 54 +++ ...ServerUseSsl-validation-options-dialog.svg | 39 ++ .../IISU-custom-field-WinRM Port-dialog.svg | 49 +++ ...d-WinRM Port-validation-options-dialog.svg | 39 ++ ...ISU-custom-field-WinRM Protocol-dialog.svg | 49 +++ ...nRM Protocol-validation-options-dialog.svg | 39 ++ .../IISU-custom-field-spnwithport-dialog.svg | 54 +++ ...-spnwithport-validation-options-dialog.svg | 39 ++ .../IISU-custom-fields-store-type-dialog.svg | 98 +++++ ...ype-dialog-HostName-validation-options.svg | 68 +++ ...-parameters-store-type-dialog-HostName.svg | 52 +++ ...pe-dialog-IPAddress-validation-options.svg | 68 +++ ...parameters-store-type-dialog-IPAddress.svg | 52 +++ ...re-type-dialog-Port-validation-options.svg | 68 +++ ...ntry-parameters-store-type-dialog-Port.svg | 52 +++ ...ype-dialog-Protocol-validation-options.svg | 68 +++ ...-parameters-store-type-dialog-Protocol.svg | 52 +++ ...dialog-ProviderName-validation-options.svg | 68 +++ ...ameters-store-type-dialog-ProviderName.svg | 52 +++ ...ype-dialog-SiteName-validation-options.svg | 68 +++ ...-parameters-store-type-dialog-SiteName.svg | 52 +++ ...type-dialog-SniFlag-validation-options.svg | 68 +++ ...y-parameters-store-type-dialog-SniFlag.svg | 52 +++ ...ISU-entry-parameters-store-type-dialog.svg | 107 +++++ .../WinAdfs-advanced-store-type-dialog.svg | 67 +++ .../WinAdfs-basic-store-type-dialog.svg | 83 ++++ ...nAdfs-custom-field-ServerUseSsl-dialog.svg | 54 +++ ...ServerUseSsl-validation-options-dialog.svg | 39 ++ ...WinAdfs-custom-field-WinRM Port-dialog.svg | 49 +++ ...d-WinRM Port-validation-options-dialog.svg | 39 ++ ...dfs-custom-field-WinRM Protocol-dialog.svg | 49 +++ ...nRM Protocol-validation-options-dialog.svg | 39 ++ ...inAdfs-custom-field-spnwithport-dialog.svg | 54 +++ ...-spnwithport-validation-options-dialog.svg | 39 ++ ...inAdfs-custom-fields-store-type-dialog.svg | 98 +++++ ...dialog-ProviderName-validation-options.svg | 68 +++ ...ameters-store-type-dialog-ProviderName.svg | 51 +++ ...dfs-entry-parameters-store-type-dialog.svg | 54 +++ .../WinCert-advanced-store-type-dialog.svg | 67 +++ .../WinCert-basic-store-type-dialog.svg | 84 ++++ ...nCert-custom-field-ServerUseSsl-dialog.svg | 54 +++ ...ServerUseSsl-validation-options-dialog.svg | 39 ++ ...WinCert-custom-field-WinRM Port-dialog.svg | 49 +++ ...d-WinRM Port-validation-options-dialog.svg | 39 ++ ...ert-custom-field-WinRM Protocol-dialog.svg | 49 +++ ...nRM Protocol-validation-options-dialog.svg | 39 ++ ...inCert-custom-field-spnwithport-dialog.svg | 54 +++ ...-spnwithport-validation-options-dialog.svg | 39 ++ ...inCert-custom-fields-store-type-dialog.svg | 98 +++++ ...dialog-ProviderName-validation-options.svg | 68 +++ ...ameters-store-type-dialog-ProviderName.svg | 51 +++ ...ert-entry-parameters-store-type-dialog.svg | 54 +++ .../WinSql-advanced-store-type-dialog.svg | 67 +++ .../images/WinSql-basic-store-type-dialog.svg | 84 ++++ ...Sql-custom-field-RestartService-dialog.svg | 54 +++ ...startService-validation-options-dialog.svg | 39 ++ ...inSql-custom-field-ServerUseSsl-dialog.svg | 54 +++ ...ServerUseSsl-validation-options-dialog.svg | 39 ++ .../WinSql-custom-field-WinRM Port-dialog.svg | 49 +++ ...d-WinRM Port-validation-options-dialog.svg | 39 ++ ...Sql-custom-field-WinRM Protocol-dialog.svg | 49 +++ ...nRM Protocol-validation-options-dialog.svg | 39 ++ ...WinSql-custom-field-spnwithport-dialog.svg | 54 +++ ...-spnwithport-validation-options-dialog.svg | 39 ++ ...WinSql-custom-fields-store-type-dialog.svg | 107 +++++ ...dialog-InstanceName-validation-options.svg | 68 +++ ...ameters-store-type-dialog-InstanceName.svg | 52 +++ ...dialog-ProviderName-validation-options.svg | 68 +++ ...ameters-store-type-dialog-ProviderName.svg | 52 +++ ...Sql-entry-parameters-store-type-dialog.svg | 62 +++ .../bash/curl_create_store_types.sh | 253 ++++++----- .../bash/kfutil_create_store_types.sh | 39 +- .../powershell/kfutil_create_store_types.ps1 | 37 +- .../restmethod_create_store_types.ps1 | 236 +++++----- 77 files changed, 4588 insertions(+), 519 deletions(-) create mode 100644 docsource/images/IISU-advanced-store-type-dialog.svg create mode 100644 docsource/images/IISU-basic-store-type-dialog.svg create mode 100644 docsource/images/IISU-custom-field-ServerUseSsl-dialog.svg create mode 100644 docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg create mode 100644 docsource/images/IISU-custom-field-WinRM Port-dialog.svg create mode 100644 docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.svg create mode 100644 docsource/images/IISU-custom-field-WinRM Protocol-dialog.svg create mode 100644 docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.svg create mode 100644 docsource/images/IISU-custom-field-spnwithport-dialog.svg create mode 100644 docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.svg create mode 100644 docsource/images/IISU-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-HostName.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-Port.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.svg create mode 100644 docsource/images/IISU-entry-parameters-store-type-dialog.svg create mode 100644 docsource/images/WinAdfs-advanced-store-type-dialog.svg create mode 100644 docsource/images/WinAdfs-basic-store-type-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-WinRM Port-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-spnwithport-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.svg create mode 100644 docsource/images/WinAdfs-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.svg create mode 100644 docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.svg create mode 100644 docsource/images/WinAdfs-entry-parameters-store-type-dialog.svg create mode 100644 docsource/images/WinCert-advanced-store-type-dialog.svg create mode 100644 docsource/images/WinCert-basic-store-type-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-ServerUseSsl-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-WinRM Port-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-WinRM Protocol-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-spnwithport-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.svg create mode 100644 docsource/images/WinCert-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.svg create mode 100644 docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.svg create mode 100644 docsource/images/WinCert-entry-parameters-store-type-dialog.svg create mode 100644 docsource/images/WinSql-advanced-store-type-dialog.svg create mode 100644 docsource/images/WinSql-basic-store-type-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-RestartService-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-ServerUseSsl-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-WinRM Port-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-WinRM Protocol-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-spnwithport-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.svg create mode 100644 docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.svg create mode 100644 docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.svg create mode 100644 docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.svg create mode 100644 docsource/images/WinSql-entry-parameters-store-type-dialog.svg diff --git a/README.md b/README.md index 11a190c9..8a3e8801 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -70,21 +70,17 @@ In version 2.0 of the IIS Orchestrator, the certificate store type has been rena **Note: If Looking to use GMSA Accounts to run the Service Keyfactor Command 10.2 or greater is required for No Value checkbox to work** The Windows Certificate Universal Orchestrator extension implements 4 Certificate Store Types. Depending on your use case, you may elect to use one, or all of these Certificate Store Types. Descriptions of each are provided below. - - [Windows Certificate](#WinCert) - - [IIS Bound Certificate](#IISU) - - [WinSql](#WinSql) - - [ADFS Rotation Manager](#WinAdfs) - ## Compatibility This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later. ## Support + The Windows Certificate Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. > If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab. @@ -93,7 +89,6 @@ The Windows Certificate Universal Orchestrator extension is supported by Keyfact Before installing the Windows Certificate Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command. -

Using the WinCert Extension on Linux servers and/or with Docker Containers: @@ -211,7 +206,6 @@ On the **target server**, open an elevated PowerShell prompt (Run as Administrat ```powershell - ## Certificate Store Types To use the Windows Certificate Universal Orchestrator extension, you **must** create the Certificate Store Types required for your use-case. This only needs to happen _once_ per Keyfactor Command instance. @@ -222,7 +216,6 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat
Click to expand details - The Windows Certificate Certificate Store Type, known by its short name 'WinCert,' enables the management of certificates within the Windows local machine certificate stores. This store type is a versatile option for general Windows certificate management and supports functionalities including inventory, add, remove, and reenrollment of certificates. The store type represents the various certificate stores present on a Windows Server. Users can specify these stores by entering the correct store path. To get a complete list of available certificate stores, the PowerShell command `Get-ChildItem Cert:\LocalMachine` can be executed, providing the actual certificate store names needed for configuration. @@ -235,24 +228,22 @@ The store type represents the various certificate stores present on a Windows Se - **Limitations:** Users should be aware that for this store type to function correctly, certain permissions are necessary. While some advanced users successfully use non-administrator accounts with specific permissions, it is officially supported only with Local Administrator permissions. Complexities with interactions between Group Policy, WinRM, User Account Control, and other environmental factors may impede operations if not properly configured. - - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | ✅ Checked | -| Discovery | 🔲 Unchecked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | | Reenrollment | ✅ Checked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +
Click to expand WinCert kfutil details ##### Using online definition from GitHub: @@ -271,10 +262,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the WinCert store type manually in the Keyfactor Command Portal +
Click to expand manual WinCert details Create a store type called `WinCert` with the attributes in the tables below: @@ -285,11 +276,11 @@ the Keyfactor Command Portal | Name | Windows Certificate | Display name for the store type (may be customized) | | Short Name | WinCert | Short display name for the store type | | Capability | WinCert | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -298,18 +289,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![WinCert Basic Tab](docsource/images/WinCert-basic-store-type-dialog.png) + ![WinCert Basic Tab](docsource/images/WinCert-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Forbidden | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![WinCert Advanced Tab](docsource/images/WinCert-advanced-store-type-dialog.png) + ![WinCert Advanced Tab](docsource/images/WinCert-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -327,31 +318,27 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![WinCert Custom Fields Tab](docsource/images/WinCert-custom-fields-store-type-dialog.png) - + ![WinCert Custom Fields Tab](docsource/images/WinCert-custom-fields-store-type-dialog.svg) ###### SPN With Port Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations. - ![WinCert Custom Field - spnwithport](docsource/images/WinCert-custom-field-spnwithport-dialog.png) - ![WinCert Custom Field - spnwithport](docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.png) - + ![WinCert Custom Field - spnwithport](docsource/images/WinCert-custom-field-spnwithport-dialog.svg) + ![WinCert Custom Field - spnwithport](docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.svg) ###### WinRM Protocol Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment. - ![WinCert Custom Field - WinRM Protocol](docsource/images/WinCert-custom-field-WinRM Protocol-dialog.png) - ![WinCert Custom Field - WinRM Protocol](docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.png) - + ![WinCert Custom Field - WinRM Protocol](docsource/images/WinCert-custom-field-WinRM Protocol-dialog.svg) + ![WinCert Custom Field - WinRM Protocol](docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.svg) ###### WinRM Port String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22. - ![WinCert Custom Field - WinRM Port](docsource/images/WinCert-custom-field-WinRM Port-dialog.png) - ![WinCert Custom Field - WinRM Port](docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.png) - + ![WinCert Custom Field - WinRM Port](docsource/images/WinCert-custom-field-WinRM Port-dialog.svg) + ![WinCert Custom Field - WinRM Port](docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.svg) ###### Server Username @@ -362,8 +349,6 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Server Password Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) @@ -372,16 +357,11 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Use SSL Determine whether the server uses SSL or not (This field is automatically created) - ![WinCert Custom Field - ServerUseSsl](docsource/images/WinCert-custom-field-ServerUseSsl-dialog.png) - ![WinCert Custom Field - ServerUseSsl](docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.png) - - - + ![WinCert Custom Field - ServerUseSsl](docsource/images/WinCert-custom-field-ServerUseSsl-dialog.svg) + ![WinCert Custom Field - ServerUseSsl](docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -392,15 +372,12 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![WinCert Entry Parameters Tab](docsource/images/WinCert-entry-parameters-store-type-dialog.png) - - + ![WinCert Entry Parameters Tab](docsource/images/WinCert-entry-parameters-store-type-dialog.svg) ##### Crypto Provider Name Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' - ![WinCert Entry Parameter - ProviderName](docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.png) - ![WinCert Entry Parameter - ProviderName](docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.png) - + ![WinCert Entry Parameter - ProviderName](docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.svg) + ![WinCert Entry Parameter - ProviderName](docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.svg)
@@ -410,7 +387,6 @@ the Keyfactor Command Portal
Click to expand details - #### Key Features and Representation The IISU store type represents the IIS servers and their certificate bindings. It specifically caters to managing SSL/TLS certificates tied to IIS websites, allowing bind operations such as specifying site names, IP addresses, ports, and enabling Server Name Indication (SNI). By default, it supports job types like Inventory, Add, Remove, and Reenrollment, thereby offering comprehensive management capabilities for IIS certificates. @@ -520,24 +496,22 @@ Using this flag will result in an error and the job will not complete successful - **Custom Alias and Private Keys:** The store type does not support custom aliases for individual entries and requires private keys because IIS certificates without private keys would be invalid. - - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | ✅ Checked | -| Discovery | 🔲 Unchecked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | | Reenrollment | ✅ Checked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +
Click to expand IISU kfutil details ##### Using online definition from GitHub: @@ -556,10 +530,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the IISU store type manually in the Keyfactor Command Portal +
Click to expand manual IISU details Create a store type called `IISU` with the attributes in the tables below: @@ -570,11 +544,11 @@ the Keyfactor Command Portal | Name | IIS Bound Certificate | Display name for the store type (may be customized) | | Short Name | IISU | Short display name for the store type | | Capability | IISU | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -583,18 +557,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![IISU Basic Tab](docsource/images/IISU-basic-store-type-dialog.png) + ![IISU Basic Tab](docsource/images/IISU-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Forbidden | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![IISU Advanced Tab](docsource/images/IISU-advanced-store-type-dialog.png) + ![IISU Advanced Tab](docsource/images/IISU-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -612,31 +586,27 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![IISU Custom Fields Tab](docsource/images/IISU-custom-fields-store-type-dialog.png) - + ![IISU Custom Fields Tab](docsource/images/IISU-custom-fields-store-type-dialog.svg) ###### SPN With Port Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations. - ![IISU Custom Field - spnwithport](docsource/images/IISU-custom-field-spnwithport-dialog.png) - ![IISU Custom Field - spnwithport](docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.png) - + ![IISU Custom Field - spnwithport](docsource/images/IISU-custom-field-spnwithport-dialog.svg) + ![IISU Custom Field - spnwithport](docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.svg) ###### WinRM Protocol Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment. - ![IISU Custom Field - WinRM Protocol](docsource/images/IISU-custom-field-WinRM Protocol-dialog.png) - ![IISU Custom Field - WinRM Protocol](docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.png) - + ![IISU Custom Field - WinRM Protocol](docsource/images/IISU-custom-field-WinRM Protocol-dialog.svg) + ![IISU Custom Field - WinRM Protocol](docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.svg) ###### WinRM Port String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22. - ![IISU Custom Field - WinRM Port](docsource/images/IISU-custom-field-WinRM Port-dialog.png) - ![IISU Custom Field - WinRM Port](docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.png) - + ![IISU Custom Field - WinRM Port](docsource/images/IISU-custom-field-WinRM Port-dialog.svg) + ![IISU Custom Field - WinRM Port](docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.svg) ###### Server Username @@ -647,8 +617,6 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Server Password Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) @@ -657,16 +625,11 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Use SSL Determine whether the server uses SSL or not (This field is automatically created) - ![IISU Custom Field - ServerUseSsl](docsource/images/IISU-custom-field-ServerUseSsl-dialog.png) - ![IISU Custom Field - ServerUseSsl](docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.png) - - - + ![IISU Custom Field - ServerUseSsl](docsource/images/IISU-custom-field-ServerUseSsl-dialog.svg) + ![IISU Custom Field - ServerUseSsl](docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -683,57 +646,54 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![IISU Entry Parameters Tab](docsource/images/IISU-entry-parameters-store-type-dialog.png) - - + ![IISU Entry Parameters Tab](docsource/images/IISU-entry-parameters-store-type-dialog.svg) ##### Port String value specifying the IP port to bind the certificate to for the IIS site. Example: '443' for HTTPS. - ![IISU Entry Parameter - Port](docsource/images/IISU-entry-parameters-store-type-dialog-Port.png) - ![IISU Entry Parameter - Port](docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.png) + ![IISU Entry Parameter - Port](docsource/images/IISU-entry-parameters-store-type-dialog-Port.svg) + ![IISU Entry Parameter - Port](docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.svg) ##### IP Address String value specifying the IP address to bind the certificate to for the IIS site. Example: '*' for all IP addresses or '192.168.1.1' for a specific IP address. - ![IISU Entry Parameter - IPAddress](docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.png) - ![IISU Entry Parameter - IPAddress](docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.png) + ![IISU Entry Parameter - IPAddress](docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.svg) + ![IISU Entry Parameter - IPAddress](docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.svg) ##### Host Name String value specifying the host name (host header) to bind the certificate to for the IIS site. Leave blank for all host names or enter a specific hostname such as 'www.example.com'. - ![IISU Entry Parameter - HostName](docsource/images/IISU-entry-parameters-store-type-dialog-HostName.png) - ![IISU Entry Parameter - HostName](docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.png) + ![IISU Entry Parameter - HostName](docsource/images/IISU-entry-parameters-store-type-dialog-HostName.svg) + ![IISU Entry Parameter - HostName](docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.svg) ##### IIS Site Name String value specifying the name of the IIS web site to bind the certificate to. Example: 'Default Web Site' or any custom site name such as 'MyWebsite'. - ![IISU Entry Parameter - SiteName](docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.png) - ![IISU Entry Parameter - SiteName](docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.png) + ![IISU Entry Parameter - SiteName](docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.svg) + ![IISU Entry Parameter - SiteName](docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.svg) ##### SSL Flags A 128-Bit Flag that determines what type of SSL settings you wish to use. The default is 0, meaning No SNI. For more information, check IIS documentation for the appropriate bit setting.) - ![IISU Entry Parameter - SniFlag](docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.png) - ![IISU Entry Parameter - SniFlag](docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.png) + ![IISU Entry Parameter - SniFlag](docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.svg) + ![IISU Entry Parameter - SniFlag](docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.svg) ##### Protocol Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication. - ![IISU Entry Parameter - Protocol](docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.png) - ![IISU Entry Parameter - Protocol](docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.png) + ![IISU Entry Parameter - Protocol](docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.svg) + ![IISU Entry Parameter - Protocol](docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.svg) ##### Crypto Provider Name Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' - ![IISU Entry Parameter - ProviderName](docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.png) - ![IISU Entry Parameter - ProviderName](docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.png) - + ![IISU Entry Parameter - ProviderName](docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.svg) + ![IISU Entry Parameter - ProviderName](docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.svg)
@@ -743,7 +703,6 @@ the Keyfactor Command Portal
Click to expand details - The WinSql Certificate Store Type, referred to by its short name 'WinSql,' is designed for the management of certificates used by SQL Server instances. This store type allows users to automate the process of adding, removing, reenrolling, and inventorying certificates associated with SQL Server, thereby simplifying the management of SSL/TLS certificates for database servers. #### Caveats and Limitations @@ -752,24 +711,22 @@ The WinSql Certificate Store Type, referred to by its short name 'WinSql,' is de - **Limitations:** Users should be aware that for this store type to function correctly, certain permissions are necessary. While some advanced users successfully use non-administrator accounts with specific permissions, it is officially supported only with Local Administrator permissions. Complexities with interactions between Group Policy, WinRM, User Account Control, and other environmental factors may impede operations if not properly configured. - - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | ✅ Checked | -| Discovery | 🔲 Unchecked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | | Reenrollment | 🔲 Unchecked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +
Click to expand WinSql kfutil details ##### Using online definition from GitHub: @@ -788,10 +745,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the WinSql store type manually in the Keyfactor Command Portal +
Click to expand manual WinSql details Create a store type called `WinSql` with the attributes in the tables below: @@ -802,11 +759,11 @@ the Keyfactor Command Portal | Name | WinSql | Display name for the store type (may be customized) | | Short Name | WinSql | Short display name for the store type | | Capability | WinSql | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | ✅ Checked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -815,18 +772,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![WinSql Basic Tab](docsource/images/WinSql-basic-store-type-dialog.png) + ![WinSql Basic Tab](docsource/images/WinSql-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Forbidden | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![WinSql Advanced Tab](docsource/images/WinSql-advanced-store-type-dialog.png) + ![WinSql Advanced Tab](docsource/images/WinSql-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -845,31 +802,27 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![WinSql Custom Fields Tab](docsource/images/WinSql-custom-fields-store-type-dialog.png) - + ![WinSql Custom Fields Tab](docsource/images/WinSql-custom-fields-store-type-dialog.svg) ###### SPN With Port Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations. - ![WinSql Custom Field - spnwithport](docsource/images/WinSql-custom-field-spnwithport-dialog.png) - ![WinSql Custom Field - spnwithport](docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.png) - + ![WinSql Custom Field - spnwithport](docsource/images/WinSql-custom-field-spnwithport-dialog.svg) + ![WinSql Custom Field - spnwithport](docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.svg) ###### WinRM Protocol Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment. - ![WinSql Custom Field - WinRM Protocol](docsource/images/WinSql-custom-field-WinRM Protocol-dialog.png) - ![WinSql Custom Field - WinRM Protocol](docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.png) - + ![WinSql Custom Field - WinRM Protocol](docsource/images/WinSql-custom-field-WinRM Protocol-dialog.svg) + ![WinSql Custom Field - WinRM Protocol](docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.svg) ###### WinRM Port String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22. - ![WinSql Custom Field - WinRM Port](docsource/images/WinSql-custom-field-WinRM Port-dialog.png) - ![WinSql Custom Field - WinRM Port](docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.png) - + ![WinSql Custom Field - WinRM Port](docsource/images/WinSql-custom-field-WinRM Port-dialog.svg) + ![WinSql Custom Field - WinRM Port](docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.svg) ###### Server Username @@ -880,8 +833,6 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Server Password Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) @@ -890,24 +841,18 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Use SSL Determine whether the server uses SSL or not (This field is automatically created) - ![WinSql Custom Field - ServerUseSsl](docsource/images/WinSql-custom-field-ServerUseSsl-dialog.png) - ![WinSql Custom Field - ServerUseSsl](docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.png) - + ![WinSql Custom Field - ServerUseSsl](docsource/images/WinSql-custom-field-ServerUseSsl-dialog.svg) + ![WinSql Custom Field - ServerUseSsl](docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.svg) ###### Restart SQL Service After Cert Installed Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation. - ![WinSql Custom Field - RestartService](docsource/images/WinSql-custom-field-RestartService-dialog.png) - ![WinSql Custom Field - RestartService](docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.png) - - - + ![WinSql Custom Field - RestartService](docsource/images/WinSql-custom-field-RestartService-dialog.svg) + ![WinSql Custom Field - RestartService](docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -919,22 +864,19 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![WinSql Entry Parameters Tab](docsource/images/WinSql-entry-parameters-store-type-dialog.png) - - + ![WinSql Entry Parameters Tab](docsource/images/WinSql-entry-parameters-store-type-dialog.svg) ##### Instance Name String value specifying the SQL Server instance name to bind the certificate to. Example: 'MSSQLServer' for the default instance or 'Instance1' for a named instance. - ![WinSql Entry Parameter - InstanceName](docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.png) - ![WinSql Entry Parameter - InstanceName](docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.png) + ![WinSql Entry Parameter - InstanceName](docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.svg) + ![WinSql Entry Parameter - InstanceName](docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.svg) ##### Crypto Provider Name Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' - ![WinSql Entry Parameter - ProviderName](docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.png) - ![WinSql Entry Parameter - ProviderName](docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.png) - + ![WinSql Entry Parameter - ProviderName](docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.svg) + ![WinSql Entry Parameter - ProviderName](docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.svg)
@@ -944,35 +886,30 @@ the Keyfactor Command Portal
Click to expand details - WinADFS is a store type designed for managing certificates within Microsoft Active Directory Federation Services (ADFS) environments. This store type enables users to automate the management of certificates used for securing ADFS communications, including tasks such as adding, removing, and renewing certificates associated with ADFS services. * NOTE: Only the Service-Communications certificate is currently supported. Follow your ADFS best practices for token encrypt and decrypt certificate management. * NOTE: This extension also supports the auto-removal of expired certificates from the ADFS stores on the Primary and Secondary nodes during the certificate rotation process, along with restarting the ADFS service to apply changes. - - - #### ADFS Rotation Manager Requirements When using WinADFS, the Universal Orchestrator must act as an agent and be installed on the Primary ADFS server within the ADFS farm. This is necessary because ADFS configurations and certificate management operations must be performed directly on the ADFS server itself to ensure proper functionality and security. - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | 🔲 Unchecked | -| Discovery | 🔲 Unchecked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | 🔲 Unchecked | +| Discovery | 🔲 Unchecked | | Reenrollment | 🔲 Unchecked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +
Click to expand WinAdfs kfutil details ##### Using online definition from GitHub: @@ -991,10 +928,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the WinAdfs store type manually in the Keyfactor Command Portal +
Click to expand manual WinAdfs details Create a store type called `WinAdfs` with the attributes in the tables below: @@ -1005,11 +942,11 @@ the Keyfactor Command Portal | Name | ADFS Rotation Manager | Display name for the store type (may be customized) | | Short Name | WinAdfs | Short display name for the store type | | Capability | WinAdfs | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | 🔲 Unchecked | Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | 🔲 Unchecked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | ✅ Checked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -1018,18 +955,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![WinAdfs Basic Tab](docsource/images/WinAdfs-basic-store-type-dialog.png) + ![WinAdfs Basic Tab](docsource/images/WinAdfs-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Forbidden | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![WinAdfs Advanced Tab](docsource/images/WinAdfs-advanced-store-type-dialog.png) + ![WinAdfs Advanced Tab](docsource/images/WinAdfs-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -1047,31 +984,27 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![WinAdfs Custom Fields Tab](docsource/images/WinAdfs-custom-fields-store-type-dialog.png) - + ![WinAdfs Custom Fields Tab](docsource/images/WinAdfs-custom-fields-store-type-dialog.svg) ###### SPN With Port Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations. - ![WinAdfs Custom Field - spnwithport](docsource/images/WinAdfs-custom-field-spnwithport-dialog.png) - ![WinAdfs Custom Field - spnwithport](docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.png) - + ![WinAdfs Custom Field - spnwithport](docsource/images/WinAdfs-custom-field-spnwithport-dialog.svg) + ![WinAdfs Custom Field - spnwithport](docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.svg) ###### WinRM Protocol Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment. - ![WinAdfs Custom Field - WinRM Protocol](docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.png) - ![WinAdfs Custom Field - WinRM Protocol](docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.png) - + ![WinAdfs Custom Field - WinRM Protocol](docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.svg) + ![WinAdfs Custom Field - WinRM Protocol](docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.svg) ###### WinRM Port String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22. - ![WinAdfs Custom Field - WinRM Port](docsource/images/WinAdfs-custom-field-WinRM Port-dialog.png) - ![WinAdfs Custom Field - WinRM Port](docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.png) - + ![WinAdfs Custom Field - WinRM Port](docsource/images/WinAdfs-custom-field-WinRM Port-dialog.svg) + ![WinAdfs Custom Field - WinRM Port](docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.svg) ###### Server Username @@ -1082,8 +1015,6 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Server Password Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) @@ -1092,16 +1023,11 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Use SSL Determine whether the server uses SSL or not (This field is automatically created) - ![WinAdfs Custom Field - ServerUseSsl](docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.png) - ![WinAdfs Custom Field - ServerUseSsl](docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.png) - - - + ![WinAdfs Custom Field - ServerUseSsl](docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.svg) + ![WinAdfs Custom Field - ServerUseSsl](docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -1112,15 +1038,12 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![WinAdfs Entry Parameters Tab](docsource/images/WinAdfs-entry-parameters-store-type-dialog.png) - - + ![WinAdfs Entry Parameters Tab](docsource/images/WinAdfs-entry-parameters-store-type-dialog.svg) ##### Crypto Provider Name Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' - ![WinAdfs Entry Parameter - ProviderName](docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.png) - ![WinAdfs Entry Parameter - ProviderName](docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.png) - + ![WinAdfs Entry Parameter - ProviderName](docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.svg) + ![WinAdfs Entry Parameter - ProviderName](docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.svg)
@@ -1131,14 +1054,15 @@ the Keyfactor Command Portal 1. **Download the latest Windows Certificate Universal Orchestrator extension from GitHub.** - Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/iis-orchestrator/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/Windows Certificate Orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `iis-orchestrator` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `Windows Certificate Orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | | Older than `11.0.0` | | | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` || Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | - | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. @@ -1151,34 +1075,29 @@ the Keyfactor Command Portal 3. **Create a new directory for the Windows Certificate Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `iis-orchestrator`. + Create a new directory called `Windows Certificate Orchestrator`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `iis-orchestrator` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `Windows Certificate Orchestrator` directory.** 5. **Restart the Universal Orchestrator service.** Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm). - 6. **(optional) PAM Integration** The Windows Certificate Universal Orchestrator extension is compatible with all supported Keyfactor PAM extensions to resolve PAM-eligible secrets. PAM extensions running on Universal Orchestrators enable secure retrieval of secrets from a connected PAM provider. To configure a PAM provider, [reference the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) to select an extension and follow the associated instructions to install it on the Universal Orchestrator (remote). - > The above installation steps can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions). - - ## Defining Certificate Stores The Windows Certificate Universal Orchestrator extension implements 4 Certificate Store Types, each of which implements different functionality. Refer to the individual instructions below for each Certificate Store Type that you deemed necessary for your use case from the installation section.
Windows Certificate (WinCert) - ### Store Creation #### Manually with the Command UI @@ -1193,8 +1112,8 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "Windows Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -1209,8 +1128,6 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat
- - #### Using kfutil CLI
Click to expand details @@ -1246,7 +1163,6 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -1262,15 +1178,12 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). -
IIS Bound Certificate (IISU) - ### Store Creation #### Manually with the Command UI @@ -1285,8 +1198,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "IIS Bound Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the IIS certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -1301,8 +1214,6 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- - #### Using kfutil CLI
Click to expand details @@ -1338,7 +1249,6 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -1354,15 +1264,12 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). -
WinSql (WinSql) - ### Store Creation #### Manually with the Command UI @@ -1377,8 +1284,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "WinSql" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the SQL Server Certificate Store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -1394,8 +1301,6 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- - #### Using kfutil CLI
Click to expand details @@ -1432,7 +1337,6 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -1448,17 +1352,14 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). -
ADFS Rotation Manager (WinAdfs) When creating a Certificate Store for WinADFS, the Client Machine name must be set as an agent and use the LocalMachine moniker, for example: myADFSPrimary|LocalMachine. - ### Store Creation #### Manually with the Command UI @@ -1473,8 +1374,8 @@ When creating a Certificate Store for WinADFS, the Client Machine name must be s Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "ADFS Rotation Manager" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Since this extension type must run as an agent (The UO Must be installed on the PRIMARY ADFS Server), the ClientMachine must follow the naming convention as outlined in the Client Machine Instructions. Secondary ADFS Nodes will be automatically be updated with the same certificate added on the PRIMARY ADFS server. | @@ -1489,8 +1390,6 @@ When creating a Certificate Store for WinADFS, the Client Machine name must be s
- - #### Using kfutil CLI
Click to expand details @@ -1526,7 +1425,6 @@ When creating a Certificate Store for WinADFS, the Client Machine name must be s
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -1542,24 +1440,20 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). -
- ## Client Machine Instructions Prior to version 2.6, this extension would only run in the Windows environment. Version 2.6 and greater is capable of running on Linux, however, only the SSH protocol is supported. If running as an agent (accessing stores on the server where the Universal Orchestrator Services is installed ONLY), the Client Machine can be entered, OR you can bypass a WinRM connection and access the local file system directly by adding "|LocalMachine" to the end of your value for Client Machine, for example "1.1.1.1|LocalMachine". In this instance the value to the left of the pipe (|) is ignored. It is important to make sure the values for Client Machine and Store Path together are unique for each certificate store created, as Keyfactor Command requires the Store Type you select, along with Client Machine, and Store Path together must be unique. To ensure this, it is good practice to put the full DNS or IP Address to the left of the | character when setting up a certificate store that will be accessed without a WinRM connection. - ## License Apache License 2.0, see [LICENSE](LICENSE). ## Related Integrations -See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). \ No newline at end of file +See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). diff --git a/docsource/images/IISU-advanced-store-type-dialog.svg b/docsource/images/IISU-advanced-store-type-dialog.svg new file mode 100644 index 00000000..cd007bfc --- /dev/null +++ b/docsource/images/IISU-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + + Forbidden + + Optional + + Required + Private Key Handling + + Forbidden + + Optional + + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/IISU-basic-store-type-dialog.svg b/docsource/images/IISU-basic-store-type-dialog.svg new file mode 100644 index 00000000..d9f1adc5 --- /dev/null +++ b/docsource/images/IISU-basic-store-type-dialog.svg @@ -0,0 +1,84 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + IIS Bound Certificate + Short Name + + IISU + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + + Remove + + Create + + Discovery + + + ODKG + + + + General Settings + + + + Needs Server + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-ServerUseSsl-dialog.svg b/docsource/images/IISU-custom-field-ServerUseSsl-dialog.svg new file mode 100644 index 00000000..d3cc61a9 --- /dev/null +++ b/docsource/images/IISU-custom-field-ServerUseSsl-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ServerUseSsl + Display Name + + Use SSL + Type + + Bool + + Default Value + + + True + + False + + Not Set + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-WinRM Port-dialog.svg b/docsource/images/IISU-custom-field-WinRM Port-dialog.svg new file mode 100644 index 00000000..11c4d673 --- /dev/null +++ b/docsource/images/IISU-custom-field-WinRM Port-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Port + Display Name + + WinRM Port + Type + + String + + Default Value + + 5986 + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.svg b/docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/IISU-custom-field-WinRM Port-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-WinRM Protocol-dialog.svg b/docsource/images/IISU-custom-field-WinRM Protocol-dialog.svg new file mode 100644 index 00000000..b03fe0a8 --- /dev/null +++ b/docsource/images/IISU-custom-field-WinRM Protocol-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Protocol + Display Name + + WinRM Protocol + Type + + MultipleChoice + + Multiple Choice Options + + https,http,ssh + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.svg b/docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/IISU-custom-field-WinRM Protocol-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-spnwithport-dialog.svg b/docsource/images/IISU-custom-field-spnwithport-dialog.svg new file mode 100644 index 00000000..163e7dba --- /dev/null +++ b/docsource/images/IISU-custom-field-spnwithport-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + spnwithport + Display Name + + SPN With Port + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + WinRM Protocol + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.svg b/docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/IISU-custom-field-spnwithport-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-fields-store-type-dialog.svg b/docsource/images/IISU-custom-fields-store-type-dialog.svg new file mode 100644 index 00000000..b0073a5c --- /dev/null +++ b/docsource/images/IISU-custom-fields-store-type-dialog.svg @@ -0,0 +1,98 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 6 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + SPN With Port + Bool + false + + + + + + + WinRM Protocol + MultipleChoice + https,http,ssh + + + + + + + WinRM Port + String + 5986 + + + + + + + Server Username + Secret + + + + + + + Server Password + Secret + + + + + + + Use SSL + Bool + true + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-HostName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-HostName.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-HostName.svg new file mode 100644 index 00000000..5e2f0f3f --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-HostName.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + HostName + Display Name + + Host Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.svg new file mode 100644 index 00000000..32869498 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + Optional + + + Required + + Hidden + Removing an entry + + Optional + + + Required + + Hidden + On Device Key Generation + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.svg new file mode 100644 index 00000000..32b247f2 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-IPAddress.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + IPAddress + Display Name + + IP Address + Type + + String + + Default Value + + * + Multiple Choice Options + + + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-Port-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-Port.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-Port.svg new file mode 100644 index 00000000..8397ea02 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-Port.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + Port + Display Name + + Port + Type + + String + + Default Value + + 443 + Multiple Choice Options + + + Depends On + + + IP Address + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.svg new file mode 100644 index 00000000..32869498 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + Optional + + + Required + + Hidden + Removing an entry + + Optional + + + Required + + Hidden + On Device Key Generation + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.svg new file mode 100644 index 00000000..ddc14a7f --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-Protocol.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + Protocol + Display Name + + Protocol + Type + + MultipleChoice + + Default Value + + https + Multiple Choice Options + + https,http + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.svg new file mode 100644 index 00000000..52d31b31 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-ProviderName.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + ProviderName + Display Name + + Crypto Provider Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.svg new file mode 100644 index 00000000..32869498 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + Optional + + + Required + + Hidden + Removing an entry + + Optional + + + Required + + Hidden + On Device Key Generation + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.svg new file mode 100644 index 00000000..93f013c0 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-SiteName.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + SiteName + Display Name + + IIS Site Name + Type + + String + + Default Value + + Default Web Site + Multiple Choice Options + + + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.svg b/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.svg new file mode 100644 index 00000000..6093aafa --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog-SniFlag.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + SniFlag + Display Name + + SSL Flags + Type + + String + + Default Value + + 0 + Multiple Choice Options + + + Depends On + + + Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog.svg b/docsource/images/IISU-entry-parameters-store-type-dialog.svg new file mode 100644 index 00000000..60f71e91 --- /dev/null +++ b/docsource/images/IISU-entry-parameters-store-type-dialog.svg @@ -0,0 +1,107 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 7 + + + Display Name + Type + Default Value + + + + + + + + + + + Port + String + 443 + + + + + + + IP Address + String + * + + + + + + + Host Name + String + + + + + + + IIS Site Name + String + Default Web Site + + + + + + + SSL Flags + String + 0 + + + + + + + Protocol + MultipleChoice + https + + + + + + + Crypto Provider Name + String + \ No newline at end of file diff --git a/docsource/images/WinAdfs-advanced-store-type-dialog.svg b/docsource/images/WinAdfs-advanced-store-type-dialog.svg new file mode 100644 index 00000000..cd007bfc --- /dev/null +++ b/docsource/images/WinAdfs-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + + Forbidden + + Optional + + Required + Private Key Handling + + Forbidden + + Optional + + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/WinAdfs-basic-store-type-dialog.svg b/docsource/images/WinAdfs-basic-store-type-dialog.svg new file mode 100644 index 00000000..ceb6ed09 --- /dev/null +++ b/docsource/images/WinAdfs-basic-store-type-dialog.svg @@ -0,0 +1,83 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + ADFS Rotation Manager + Short Name + + WinAdfs + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + Remove + + Create + + Discovery + + ODKG + + + + General Settings + + + + Needs Server + + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.svg b/docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.svg new file mode 100644 index 00000000..d3cc61a9 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-ServerUseSsl-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ServerUseSsl + Display Name + + Use SSL + Type + + Bool + + Default Value + + + True + + False + + Not Set + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-ServerUseSsl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-WinRM Port-dialog.svg b/docsource/images/WinAdfs-custom-field-WinRM Port-dialog.svg new file mode 100644 index 00000000..11c4d673 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-WinRM Port-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Port + Display Name + + WinRM Port + Type + + String + + Default Value + + 5986 + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.svg b/docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-WinRM Port-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.svg b/docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.svg new file mode 100644 index 00000000..b03fe0a8 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-WinRM Protocol-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Protocol + Display Name + + WinRM Protocol + Type + + MultipleChoice + + Multiple Choice Options + + https,http,ssh + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.svg b/docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-WinRM Protocol-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-spnwithport-dialog.svg b/docsource/images/WinAdfs-custom-field-spnwithport-dialog.svg new file mode 100644 index 00000000..163e7dba --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-spnwithport-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + spnwithport + Display Name + + SPN With Port + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + WinRM Protocol + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.svg b/docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/WinAdfs-custom-field-spnwithport-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-custom-fields-store-type-dialog.svg b/docsource/images/WinAdfs-custom-fields-store-type-dialog.svg new file mode 100644 index 00000000..b0073a5c --- /dev/null +++ b/docsource/images/WinAdfs-custom-fields-store-type-dialog.svg @@ -0,0 +1,98 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 6 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + SPN With Port + Bool + false + + + + + + + WinRM Protocol + MultipleChoice + https,http,ssh + + + + + + + WinRM Port + String + 5986 + + + + + + + Server Username + Secret + + + + + + + Server Password + Secret + + + + + + + Use SSL + Bool + true + \ No newline at end of file diff --git a/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.svg b/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.svg b/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.svg new file mode 100644 index 00000000..bf6ae040 --- /dev/null +++ b/docsource/images/WinAdfs-entry-parameters-store-type-dialog-ProviderName.svg @@ -0,0 +1,51 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + ProviderName + Display Name + + Crypto Provider Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinAdfs-entry-parameters-store-type-dialog.svg b/docsource/images/WinAdfs-entry-parameters-store-type-dialog.svg new file mode 100644 index 00000000..167c5b5d --- /dev/null +++ b/docsource/images/WinAdfs-entry-parameters-store-type-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 1 + + + Display Name + Type + Default Value + + + + + + + + + + + Crypto Provider Name + String + \ No newline at end of file diff --git a/docsource/images/WinCert-advanced-store-type-dialog.svg b/docsource/images/WinCert-advanced-store-type-dialog.svg new file mode 100644 index 00000000..c0df7539 --- /dev/null +++ b/docsource/images/WinCert-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + + Forbidden + + Optional + + Required + Private Key Handling + + Forbidden + + + Optional + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/WinCert-basic-store-type-dialog.svg b/docsource/images/WinCert-basic-store-type-dialog.svg new file mode 100644 index 00000000..2819ca7d --- /dev/null +++ b/docsource/images/WinCert-basic-store-type-dialog.svg @@ -0,0 +1,84 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + Windows Certificate + Short Name + + WinCert + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + + Remove + + Create + + Discovery + + + ODKG + + + + General Settings + + + + Needs Server + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-ServerUseSsl-dialog.svg b/docsource/images/WinCert-custom-field-ServerUseSsl-dialog.svg new file mode 100644 index 00000000..d3cc61a9 --- /dev/null +++ b/docsource/images/WinCert-custom-field-ServerUseSsl-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ServerUseSsl + Display Name + + Use SSL + Type + + Bool + + Default Value + + + True + + False + + Not Set + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-WinRM Port-dialog.svg b/docsource/images/WinCert-custom-field-WinRM Port-dialog.svg new file mode 100644 index 00000000..11c4d673 --- /dev/null +++ b/docsource/images/WinCert-custom-field-WinRM Port-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Port + Display Name + + WinRM Port + Type + + String + + Default Value + + 5986 + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.svg b/docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinCert-custom-field-WinRM Port-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-WinRM Protocol-dialog.svg b/docsource/images/WinCert-custom-field-WinRM Protocol-dialog.svg new file mode 100644 index 00000000..b03fe0a8 --- /dev/null +++ b/docsource/images/WinCert-custom-field-WinRM Protocol-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Protocol + Display Name + + WinRM Protocol + Type + + MultipleChoice + + Multiple Choice Options + + https,http,ssh + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.svg b/docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinCert-custom-field-WinRM Protocol-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-spnwithport-dialog.svg b/docsource/images/WinCert-custom-field-spnwithport-dialog.svg new file mode 100644 index 00000000..163e7dba --- /dev/null +++ b/docsource/images/WinCert-custom-field-spnwithport-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + spnwithport + Display Name + + SPN With Port + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + WinRM Protocol + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.svg b/docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/WinCert-custom-field-spnwithport-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-fields-store-type-dialog.svg b/docsource/images/WinCert-custom-fields-store-type-dialog.svg new file mode 100644 index 00000000..b0073a5c --- /dev/null +++ b/docsource/images/WinCert-custom-fields-store-type-dialog.svg @@ -0,0 +1,98 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 6 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + SPN With Port + Bool + false + + + + + + + WinRM Protocol + MultipleChoice + https,http,ssh + + + + + + + WinRM Port + String + 5986 + + + + + + + Server Username + Secret + + + + + + + Server Password + Secret + + + + + + + Use SSL + Bool + true + \ No newline at end of file diff --git a/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.svg b/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.svg b/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.svg new file mode 100644 index 00000000..bf6ae040 --- /dev/null +++ b/docsource/images/WinCert-entry-parameters-store-type-dialog-ProviderName.svg @@ -0,0 +1,51 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + ProviderName + Display Name + + Crypto Provider Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-entry-parameters-store-type-dialog.svg b/docsource/images/WinCert-entry-parameters-store-type-dialog.svg new file mode 100644 index 00000000..167c5b5d --- /dev/null +++ b/docsource/images/WinCert-entry-parameters-store-type-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 1 + + + Display Name + Type + Default Value + + + + + + + + + + + Crypto Provider Name + String + \ No newline at end of file diff --git a/docsource/images/WinSql-advanced-store-type-dialog.svg b/docsource/images/WinSql-advanced-store-type-dialog.svg new file mode 100644 index 00000000..c0df7539 --- /dev/null +++ b/docsource/images/WinSql-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + + Forbidden + + Optional + + Required + Private Key Handling + + Forbidden + + + Optional + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/WinSql-basic-store-type-dialog.svg b/docsource/images/WinSql-basic-store-type-dialog.svg new file mode 100644 index 00000000..57755d51 --- /dev/null +++ b/docsource/images/WinSql-basic-store-type-dialog.svg @@ -0,0 +1,84 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + WinSql + Short Name + + WinSql + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + + Remove + + Create + + Discovery + + ODKG + + + + General Settings + + + + Needs Server + + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-RestartService-dialog.svg b/docsource/images/WinSql-custom-field-RestartService-dialog.svg new file mode 100644 index 00000000..9742e467 --- /dev/null +++ b/docsource/images/WinSql-custom-field-RestartService-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + RestartService + Display Name + + Restart SQL Service After Cert Installed + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-ServerUseSsl-dialog.svg b/docsource/images/WinSql-custom-field-ServerUseSsl-dialog.svg new file mode 100644 index 00000000..d3cc61a9 --- /dev/null +++ b/docsource/images/WinSql-custom-field-ServerUseSsl-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ServerUseSsl + Display Name + + Use SSL + Type + + Bool + + Default Value + + + True + + False + + Not Set + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinSql-custom-field-ServerUseSsl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-WinRM Port-dialog.svg b/docsource/images/WinSql-custom-field-WinRM Port-dialog.svg new file mode 100644 index 00000000..11c4d673 --- /dev/null +++ b/docsource/images/WinSql-custom-field-WinRM Port-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Port + Display Name + + WinRM Port + Type + + String + + Default Value + + 5986 + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinSql-custom-field-WinRM Port-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-WinRM Protocol-dialog.svg b/docsource/images/WinSql-custom-field-WinRM Protocol-dialog.svg new file mode 100644 index 00000000..b03fe0a8 --- /dev/null +++ b/docsource/images/WinSql-custom-field-WinRM Protocol-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + WinRM Protocol + Display Name + + WinRM Protocol + Type + + MultipleChoice + + Multiple Choice Options + + https,http,ssh + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.svg new file mode 100644 index 00000000..7993c230 --- /dev/null +++ b/docsource/images/WinSql-custom-field-WinRM Protocol-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-spnwithport-dialog.svg b/docsource/images/WinSql-custom-field-spnwithport-dialog.svg new file mode 100644 index 00000000..163e7dba --- /dev/null +++ b/docsource/images/WinSql-custom-field-spnwithport-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + spnwithport + Display Name + + SPN With Port + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + WinRM Protocol + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/WinSql-custom-field-spnwithport-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-fields-store-type-dialog.svg b/docsource/images/WinSql-custom-fields-store-type-dialog.svg new file mode 100644 index 00000000..905ad22d --- /dev/null +++ b/docsource/images/WinSql-custom-fields-store-type-dialog.svg @@ -0,0 +1,107 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 7 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + SPN With Port + Bool + false + + + + + + + WinRM Protocol + MultipleChoice + https,http,ssh + + + + + + + WinRM Port + String + 5986 + + + + + + + Server Username + Secret + + + + + + + Server Password + Secret + + + + + + + Use SSL + Bool + true + + + + + + + Restart SQL Service After Cert Ins... + Bool + false + \ No newline at end of file diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.svg b/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.svg b/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.svg new file mode 100644 index 00000000..6ed9740a --- /dev/null +++ b/docsource/images/WinSql-entry-parameters-store-type-dialog-InstanceName.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + InstanceName + Display Name + + Instance Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + Crypto Provider Name + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.svg b/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.svg new file mode 100644 index 00000000..ddaa9ab0 --- /dev/null +++ b/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.svg b/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.svg new file mode 100644 index 00000000..792a632d --- /dev/null +++ b/docsource/images/WinSql-entry-parameters-store-type-dialog-ProviderName.svg @@ -0,0 +1,52 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + ProviderName + Display Name + + Crypto Provider Name + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + Instance Name + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog.svg b/docsource/images/WinSql-entry-parameters-store-type-dialog.svg new file mode 100644 index 00000000..8bb72ef6 --- /dev/null +++ b/docsource/images/WinSql-entry-parameters-store-type-dialog.svg @@ -0,0 +1,62 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 2 + + + Display Name + Type + Default Value + + + + + + + + + + + Instance Name + String + + + + + + + Crypto Provider Name + String + \ No newline at end of file diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh index eaecbf43..e3ce44bf 100755 --- a/scripts/store_types/bash/curl_create_store_types.sh +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -1,78 +1,20 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using curl +# Generated by Doctool -# Creates all 4 store types via the Keyfactor Command REST API using curl. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if [ -z "${KEYFACTOR_HOSTNAME}" ]; then - echo "ERROR: KEYFACTOR_HOSTNAME is required" - exit 1 -fi +# Configuration - set these variables before running +KEYFACTOR_HOSTNAME="${KEYFACTOR_HOSTNAME}" +KEYFACTOR_API_PATH="${KEYFACTOR_API_PATH:-KeyfactorAPI}" +KEYFACTOR_AUTH_TOKEN="${KEYFACTOR_AUTH_TOKEN}" -BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi" - -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then - BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}" -elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then - echo "Fetching OAuth token..." - BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "grant_type=client_credentials" \ - --data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \ - --data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token') - if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then - echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}" - exit 1 - fi -elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then - BEARER_TOKEN="" -else - echo "ERROR: Authentication required. Set one of:" - echo " KEYFACTOR_AUTH_ACCESS_TOKEN" - echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL" - echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN" - exit 1 -fi - -if [ -n "${BEARER_TOKEN}" ]; then - CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}") -else - CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}") -fi - -create_store_type() { - local name="$1" - local body="$2" - echo "Creating ${name} store type..." - response=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST "${BASE_URL}/certificatestoretypes" \ - -H "Content-Type: application/json" \ - -H "x-keyfactor-requested-with: APIClient" \ - "${CURL_AUTH[@]}" \ - -d "${body}") - if [ "$response" = "200" ] || [ "$response" = "201" ]; then - echo " OK (HTTP ${response})" - else - echo " FAILED (HTTP ${response})" - fi -} - -# --------------------------------------------------------------------------- -# WinCert — Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -create_store_type "WinCert" '{ +echo "Creating store type: WinCert" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ "Name": "Windows Certificate", "ShortName": "WinCert", "Capability": "WinCert", @@ -91,7 +33,8 @@ create_store_type "WinCert" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -99,7 +42,8 @@ create_store_type "WinCert" '{ "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -107,7 +51,26 @@ create_store_type "WinCert" '{ "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -115,7 +78,8 @@ create_store_type "WinCert" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -145,14 +109,15 @@ create_store_type "WinCert" '{ "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Windows certificate store path to manage. The store must exist in the Local Machine store on the target server, e.g., 'My' for the Personal Store or 'Root' for the Trusted Root Certification Authorities Store." + "CustomAliasAllowed": "Forbidden" }' -# --------------------------------------------------------------------------- -# IISU — Hostname of the Windows Server containing the IIS certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -create_store_type "IISU" '{ +echo "Creating store type: IISU" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ "Name": "IIS Bound Certificate", "ShortName": "IISU", "Capability": "IISU", @@ -171,7 +136,8 @@ create_store_type "IISU" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -179,7 +145,8 @@ create_store_type "IISU" '{ "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -187,7 +154,26 @@ create_store_type "IISU" '{ "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -195,7 +181,8 @@ create_store_type "IISU" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -315,14 +302,15 @@ create_store_type "IISU" '{ "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Windows certificate store path to manage. Choose 'My' for the Personal store or 'WebHosting' for the Web Hosting store." + "CustomAliasAllowed": "Forbidden" }' -# --------------------------------------------------------------------------- -# WinSql — Hostname of the Windows Server containing the SQL Server Certificate Store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -create_store_type "WinSql" '{ +echo "Creating store type: WinSql" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ "Name": "WinSql", "ShortName": "WinSql", "Capability": "WinSql", @@ -341,7 +329,8 @@ create_store_type "WinSql" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -349,7 +338,8 @@ create_store_type "WinSql" '{ "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -357,7 +347,26 @@ create_store_type "WinSql" '{ "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -365,7 +374,8 @@ create_store_type "WinSql" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" }, { "Name": "RestartService", @@ -373,7 +383,8 @@ create_store_type "WinSql" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": true + "Required": true, + "Description": "Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation." } ], "EntryParameters": [ @@ -415,14 +426,15 @@ create_store_type "WinSql" '{ "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Fixed string value 'My' indicating the Personal store on the Local Machine. This denotes the Windows certificate store to be managed for SQL Server." + "CustomAliasAllowed": "Forbidden" }' -# --------------------------------------------------------------------------- -# WinAdfs — Since this extension type must run as an agent (The UO Must be installed on the PRIMARY ADFS Server), the ClientMachine must follow the naming convention as outlined in the Client Machine Instructions. Secondary ADFS Nodes will be automatically be updated with the same certificate added on the PRIMARY ADFS server. -# --------------------------------------------------------------------------- -create_store_type "WinAdfs" '{ +echo "Creating store type: WinAdfs" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ "Name": "ADFS Rotation Manager", "ShortName": "WinAdfs", "Capability": "WinAdfs", @@ -441,7 +453,8 @@ create_store_type "WinAdfs" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -449,7 +462,8 @@ create_store_type "WinAdfs" '{ "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -457,7 +471,26 @@ create_store_type "WinAdfs" '{ "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -465,7 +498,8 @@ create_store_type "WinAdfs" '{ "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -495,9 +529,6 @@ create_store_type "WinAdfs" '{ "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Fixed string value of 'My' indicating the Personal store on the Local Machine. All ADFS Service-Communications certificates are located in the 'My' personal store by default." + "CustomAliasAllowed": "Forbidden" }' - -echo "Completed." diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh index fe372c14..8af630e6 100755 --- a/scripts/store_types/bash/kfutil_create_store_types.sh +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -1,31 +1,18 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using kfutil +# Generated by Doctool -# Creates all 4 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if ! command -v kfutil &> /dev/null; then - echo "kfutil could not be found. Please install kfutil" - echo "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -fi +echo "Creating store type: WinCert" +kfutil store-types create WinCert -if [ -z "$KEYFACTOR_HOSTNAME" ]; then - echo "KEYFACTOR_HOSTNAME not set — launching kfutil login" - kfutil login -fi +echo "Creating store type: IISU" +kfutil store-types create IISU -kfutil store-types create --name "WinCert" -kfutil store-types create --name "IISU" -kfutil store-types create --name "WinSql" -kfutil store-types create --name "WinAdfs" +echo "Creating store type: WinSql" +kfutil store-types create WinSql + +echo "Creating store type: WinAdfs" +kfutil store-types create WinAdfs -echo "Done. All store types created." diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1 index e8a2bf12..cbc0475b 100644 --- a/scripts/store_types/powershell/kfutil_create_store_types.ps1 +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -1,32 +1,15 @@ -# Creates all 4 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using kfutil +# Generated by Doctool -# Uncomment if kfutil is not in your PATH -# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe' +Write-Host "Creating store type: WinCert" +kfutil store-types create WinCert -if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) { - Write-Host "kfutil could not be found. Please install kfutil" - Write-Host "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -} +Write-Host "Creating store type: IISU" +kfutil store-types create IISU -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login" - & kfutil login -} +Write-Host "Creating store type: WinSql" +kfutil store-types create WinSql -& kfutil store-types create --name "WinCert" -& kfutil store-types create --name "IISU" -& kfutil store-types create --name "WinSql" -& kfutil store-types create --name "WinAdfs" +Write-Host "Creating store type: WinAdfs" +kfutil store-types create WinAdfs -Write-Host "Done. All store types created." diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 index a8118925..fefc2320 100644 --- a/scripts/store_types/powershell/restmethod_create_store_types.ps1 +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -1,70 +1,19 @@ -# Creates all 4 store types via the Keyfactor Command REST API -# using PowerShell Invoke-RestMethod. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using Invoke-RestMethod +# Generated by Doctool -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Error "KEYFACTOR_HOSTNAME is required" - exit 1 -} - -$uri = "https://$($env:KEYFACTOR_HOSTNAME)/keyfactorapi/certificatestoretypes" -$headers = @{ - 'Content-Type' = "application/json" - 'x-keyfactor-requested-with' = "APIClient" -} +# Configuration - set these variables before running +$KeyfactorHostname = $env:KEYFACTOR_HOSTNAME +$KeyfactorApiPath = if ($env:KEYFACTOR_API_PATH) { $env:KEYFACTOR_API_PATH } else { "KeyfactorAPI" } +$KeyfactorAuthToken = $env:KEYFACTOR_AUTH_TOKEN -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if ($env:KEYFACTOR_AUTH_ACCESS_TOKEN) { - $headers['Authorization'] = "Bearer $($env:KEYFACTOR_AUTH_ACCESS_TOKEN)" -} elseif ($env:KEYFACTOR_AUTH_CLIENT_ID -and $env:KEYFACTOR_AUTH_CLIENT_SECRET -and $env:KEYFACTOR_AUTH_TOKEN_URL) { - Write-Host "Fetching OAuth token..." - $tokenBody = @{ - grant_type = 'client_credentials' - client_id = $env:KEYFACTOR_AUTH_CLIENT_ID - client_secret = $env:KEYFACTOR_AUTH_CLIENT_SECRET - } - $tokenResp = Invoke-RestMethod -Method Post -Uri $env:KEYFACTOR_AUTH_TOKEN_URL -Body $tokenBody - $headers['Authorization'] = "Bearer $($tokenResp.access_token)" -} elseif ($env:KEYFACTOR_USERNAME -and $env:KEYFACTOR_PASSWORD -and $env:KEYFACTOR_DOMAIN) { - $cred = [System.Convert]::ToBase64String( - [System.Text.Encoding]::ASCII.GetBytes( - "$($env:KEYFACTOR_USERNAME)@$($env:KEYFACTOR_DOMAIN):$($env:KEYFACTOR_PASSWORD)")) - $headers['Authorization'] = "Basic $cred" -} else { - Write-Error ("Authentication required. Set one of:`n" + - " KEYFACTOR_AUTH_ACCESS_TOKEN`n" + - " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL`n" + - " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN") - exit 1 +$Headers = @{ + "Authorization" = "Bearer $KeyfactorAuthToken" + "Content-Type" = "application/json" + "x-keyfactor-requested-with" = "APIClient" } -function New-StoreType { - param([string]$Name, [string]$Body) - Write-Host "Creating $Name store type..." - try { - Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $Body -ContentType "application/json" | Out-Null - Write-Host " OK" - } catch { - Write-Warning " FAILED: $($_.Exception.Message)" - } -} - -# --------------------------------------------------------------------------- -# WinCert — Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -New-StoreType "WinCert" @' +Write-Host "Creating store type: WinCert" +$Body = @' { "Name": "Windows Certificate", "ShortName": "WinCert", @@ -84,7 +33,8 @@ New-StoreType "WinCert" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -92,7 +42,8 @@ New-StoreType "WinCert" @' "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -100,7 +51,26 @@ New-StoreType "WinCert" @' "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -108,7 +78,8 @@ New-StoreType "WinCert" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -138,15 +109,14 @@ New-StoreType "WinCert" @' "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Windows certificate store path to manage. The store must exist in the Local Machine store on the target server, e.g., 'My' for the Personal Store or 'Root' for the Trusted Root Certification Authorities Store." + "CustomAliasAllowed": "Forbidden" } '@ -# --------------------------------------------------------------------------- -# IISU — Hostname of the Windows Server containing the IIS certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -New-StoreType "IISU" @' +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body + +Write-Host "Creating store type: IISU" +$Body = @' { "Name": "IIS Bound Certificate", "ShortName": "IISU", @@ -166,7 +136,8 @@ New-StoreType "IISU" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -174,7 +145,8 @@ New-StoreType "IISU" @' "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -182,7 +154,26 @@ New-StoreType "IISU" @' "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -190,7 +181,8 @@ New-StoreType "IISU" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -310,15 +302,14 @@ New-StoreType "IISU" @' "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Windows certificate store path to manage. Choose 'My' for the Personal store or 'WebHosting' for the Web Hosting store." + "CustomAliasAllowed": "Forbidden" } '@ -# --------------------------------------------------------------------------- -# WinSql — Hostname of the Windows Server containing the SQL Server Certificate Store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). -# --------------------------------------------------------------------------- -New-StoreType "WinSql" @' +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body + +Write-Host "Creating store type: WinSql" +$Body = @' { "Name": "WinSql", "ShortName": "WinSql", @@ -338,7 +329,8 @@ New-StoreType "WinSql" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -346,7 +338,8 @@ New-StoreType "WinSql" @' "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -354,7 +347,26 @@ New-StoreType "WinSql" @' "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -362,7 +374,8 @@ New-StoreType "WinSql" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" }, { "Name": "RestartService", @@ -370,7 +383,8 @@ New-StoreType "WinSql" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": true + "Required": true, + "Description": "Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation." } ], "EntryParameters": [ @@ -412,15 +426,14 @@ New-StoreType "WinSql" @' "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Fixed string value 'My' indicating the Personal store on the Local Machine. This denotes the Windows certificate store to be managed for SQL Server." + "CustomAliasAllowed": "Forbidden" } '@ -# --------------------------------------------------------------------------- -# WinAdfs — Since this extension type must run as an agent (The UO Must be installed on the PRIMARY ADFS Server), the ClientMachine must follow the naming convention as outlined in the Client Machine Instructions. Secondary ADFS Nodes will be automatically be updated with the same certificate added on the PRIMARY ADFS server. -# --------------------------------------------------------------------------- -New-StoreType "WinAdfs" @' +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body + +Write-Host "Creating store type: WinAdfs" +$Body = @' { "Name": "ADFS Rotation Manager", "ShortName": "WinAdfs", @@ -440,7 +453,8 @@ New-StoreType "WinAdfs" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "false", - "Required": false + "Required": false, + "Description": "Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations." }, { "Name": "WinRM Protocol", @@ -448,7 +462,8 @@ New-StoreType "WinAdfs" @' "Type": "MultipleChoice", "DependsOn": "", "DefaultValue": "https,http,ssh", - "Required": true + "Required": true, + "Description": "Multiple choice value specifying which protocol to use. Protocols https or http use WinRM to connect from Windows to Windows Servers. Using ssh is only supported when running the orchestrator in a Linux environment." }, { "Name": "WinRM Port", @@ -456,7 +471,26 @@ New-StoreType "WinAdfs" @' "Type": "String", "DependsOn": "", "DefaultValue": "5986", - "Required": true + "Required": true, + "Description": "String value specifying the port number that the Windows target server's WinRM listener is configured to use. Example: '5986' for HTTPS or '5985' for HTTP. By default, when using ssh in a Linux environment, the default port number is 22." + }, + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\\username'. (This field is automatically created)" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created)" }, { "Name": "ServerUseSsl", @@ -464,7 +498,8 @@ New-StoreType "WinAdfs" @' "Type": "Bool", "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": true, + "Description": "Determine whether the server uses SSL or not (This field is automatically created)" } ], "EntryParameters": [ @@ -494,10 +529,9 @@ New-StoreType "WinAdfs" @' "ServerRequired": true, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Forbidden", - "StorePathDescription": "Fixed string value of 'My' indicating the Personal store on the Local Machine. All ADFS Service-Communications certificates are located in the 'My' personal store by default." + "CustomAliasAllowed": "Forbidden" } '@ +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body -Write-Host "Completed." From f85f910052367fd50537b00d6760ff4b089ee41b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 12 May 2026 09:33:31 -0700 Subject: [PATCH 19/53] Update generated docs --- README.md | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 330 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8a3e8801..d8461e49 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -205,6 +205,329 @@ Copy this entire `PowerShell` folder to the target server (or to a network share On the **target server**, open an elevated PowerShell prompt (Run as Administrator) and run the following commands. Adjust the source path (`$sourcePath`) to wherever you placed the `PowerShell` folder in Step 1. ```powershell +# Set the source path to where you copied the PowerShell folder +$sourcePath = 'C:\Temp\PowerShell' + +# System module path — modules installed here are treated as fully trusted by PowerShell +$moduleBase = 'C:\Program Files\WindowsPowerShell\Modules' + +# Always install the Common module — required for all store types +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.Common" ` + -Destination "$moduleBase\Keyfactor.WinCert.Common" ` + -Recurse -Force + +# Install the IIS module if this server hosts IIS certificate stores (WinIIS) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.IIS" ` + -Destination "$moduleBase\Keyfactor.WinCert.IIS" ` + -Recurse -Force + +# Install the SQL module if this server hosts SQL Server certificate stores (WinSQL) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.SQL" ` + -Destination "$moduleBase\Keyfactor.WinCert.SQL" ` + -Recurse -Force +``` + +> **Important:** Modules **must** be installed under `C:\Program Files\WindowsPowerShell\Modules\` (or another path listed in the system `$env:PSModulePath`). Modules installed outside of a trusted path will not run as fully trusted code inside a `ConstrainedLanguage` JEA session, and calls to .NET APIs will fail. + +Verify that the modules installed correctly by running: + +```powershell +Get-Module -ListAvailable | Where-Object { $_.Name -like 'Keyfactor.*' } +``` + +You should see entries for each module you installed. + +--- + +#### Step 3: Create the Audit Transcript Directory + +JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. + +```powershell +New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force +``` + +Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. + +--- + +#### Step 4: Review and Customize the Session Configuration File + +Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working location on the target server (e.g., `C:\Temp\KeyfactorWinCert.pssc`) and open it in a text editor. The key settings to review and customize are: + +**Run-As Account (choose one):** + +The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: + +- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. + + ```powershell + RunAsVirtualAccount = $true + ``` + +- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. + + ```powershell + # Comment out RunAsVirtualAccount and uncomment this line: + GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' + ``` + + To create a gMSA (run on a domain controller or with AD PowerShell module): + ```powershell + # Create the gMSA in Active Directory + New-ADServiceAccount -Name 'KeyfactorJEA' ` + -DNSHostName 'keyfactorjea.yourdomain.com' ` + -PrincipalsAllowedToRetrieveManagedPassword 'KeyfactorServers$' + + # On the target server, install the gMSA + Install-ADServiceAccount -Identity 'KeyfactorJEA$' + + # Verify the gMSA can log on + Test-ADServiceAccount -Identity 'KeyfactorJEA$' + ``` + +**Role Definitions (who is allowed to connect):** + +The `RoleDefinitions` section maps connecting users or groups to JEA role capabilities. Replace `BUILTIN\Administrators` with the specific AD group or local group whose members should be allowed to connect via JEA. Using a dedicated AD group is strongly recommended for production environments. + +```powershell +RoleDefinitions = @{ + # Replace with the AD group that contains your Keyfactor orchestrator service accounts: + 'DOMAIN\KeyfactorOrchestrators' = @{ + RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' + } +} +``` + +Only list the `RoleCapabilities` whose corresponding modules are installed on this server. The available combinations are: + +| Store Types on This Server | RoleCapabilities to List | +|---|---| +| WinCert only | `'Keyfactor.WinCert.Common'` | +| WinIIS only or WinCert + WinIIS | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS'` | +| WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | +| WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | + +--- + +#### Step 5: Register the JEA Session Configuration + +On the **target server**, in an elevated PowerShell prompt, register the session configuration. This creates the named WinRM endpoint that the Keyfactor orchestrator will connect to. + +```powershell +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +# WinRM must be restarted for the new endpoint to become active +Restart-Service WinRM +``` + +> **Note:** `Restart-Service WinRM` will briefly interrupt all active WinRM connections on the server. Schedule this during a maintenance window if other services depend on WinRM. + +The name `keyfactor.wincert` is the endpoint name you will enter into the **JEA Endpoint Name** field in Keyfactor Command. You may use a different name if desired — just ensure it matches exactly when configuring the certificate store. + +--- + +#### Step 6: Verify the Registration + +Confirm the endpoint is registered and its configuration is correct: + +```powershell +# List all registered session configurations +Get-PSSessionConfiguration | Where-Object { $_.Name -eq 'keyfactor.wincert' } +``` + +You should see output showing the configuration name, PSVersion, and the path to the `.pssc` file. + +For a more thorough validation, connect to the JEA endpoint from a remote machine and verify that the Keyfactor functions are available: + +```powershell +# Connect to the JEA endpoint (run this from the Keyfactor orchestrator server or any machine with network access) +$cred = Get-Credential # Enter the orchestrator service account credentials +$s = New-PSSession -ComputerName '' ` + -Port 5985 ` + -ConfigurationName 'keyfactor.wincert' ` + -Credential $cred + +# List all commands available in the JEA session (should be limited to Keyfactor functions only) +Invoke-Command -Session $s -ScriptBlock { Get-Command } + +# Test a certificate inventory call (WinCert) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } + +# Test IIS inventory (if Keyfactor.WinCert.IIS is installed on the target) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorIISBoundCertificates } + +# Clean up the test session +Remove-PSSession $s +``` + +The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. + +--- + +#### Step 7: Configure the Certificate Store in Keyfactor Command + +Once the JEA endpoint is registered and verified on the target server, configure the certificate store in Keyfactor Command: + +1. Navigate to the certificate store you wish to manage via JEA (or create a new one). +2. In the store's **Custom Parameters** (also called **Store Properties**), locate the **JEA Endpoint Name** field. +3. Enter the name of the JEA session configuration you registered — for example, `keyfactor.wincert`. +4. Ensure the **Client Machine** is set to the target server's hostname or IP address. **Do not** use `localhost`, `LocalMachine`, or the `|LocalMachine` suffix — JEA requires a real WinRM network connection. +5. The **Server Username** and **Server Password** fields should contain the credentials of the account that is permitted to connect to the JEA endpoint (i.e., a member of the group specified in `RoleDefinitions` in the `.pssc` file). +6. Save the certificate store. + +When a job runs against this store, the orchestrator will automatically use the JEA endpoint instead of a standard WinRM administrative session. + +--- + +### Updating the JEA Configuration + +If you need to change the session configuration — for example, to add a new module or change the run-as account — update the `.pssc` file and re-register it: + +```powershell +# Re-register with the -Force flag to overwrite the existing registration +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +Restart-Service WinRM +``` + +If you update one of the Keyfactor modules (e.g., after upgrading the WinCert extension), repeat Step 2 to copy the new module files to the target server. No re-registration of the session configuration is necessary for module-only updates — the next JEA session will load the updated module automatically. + +--- + +### Removing the JEA Configuration + +To remove the JEA endpoint from a server: + +```powershell +Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' +Restart-Service WinRM +``` + +After removing the endpoint, any certificate stores in Keyfactor Command that reference the JEA endpoint name will fail. Update those stores to either clear the **JEA Endpoint Name** field (to revert to standard WinRM) or point to a different JEA endpoint. + +--- + +### Troubleshooting + +**"JEA endpoint is reachable but Keyfactor modules are not installed"** + +The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). + +**"The term 'Get-KeyfactorCertificates' is not recognized..."** + +The function is not visible in the JEA session. Verify that: +- The module containing that function is installed on the target server (Step 2). +- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). +- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). +- The session configuration was re-registered and WinRM was restarted after any changes. + +**"Connecting user is not authorized to connect to this configuration"** + +The account used in the certificate store credentials is not a member of any group listed in `RoleDefinitions`. Add the account (or a group containing it) to the `RoleDefinitions` section in the `.pssc`, re-register the configuration, and restart WinRM. + +**"Access is denied" or "WinRM cannot complete the operation"** + +This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: +- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). +- The WinRM firewall rule allows connections from the orchestrator server's IP. +- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. + +**"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** + +A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. + +**Reviewing JEA Transcripts** + +All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. + +```powershell +# List recent transcript files +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | Sort-Object LastWriteTime -Descending | Select-Object -First 10 + +# View the most recent transcript +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content +``` + +--- + +### Important Notes and Limitations + +- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. +- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. +- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. +- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. +- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. +- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. + +

+ +Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. + +### PowerShell Requirements +PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). +In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. + +**JEA Module Requirements:** When using JEA (Just Enough Administration) to connect to a target server, the Keyfactor PowerShell modules must be pre-installed on each target server under `C:\Program Files\WindowsPowerShell\Modules\`. These modules are included in the WinCert extension deployment package inside the `PowerShell` folder. The modules that must be installed depend on which store types are managed on that server: + +| Module | Required For | +|---|---| +| `Keyfactor.WinCert.Common` | All store types — must always be installed | +| `Keyfactor.WinCert.IIS` | WinIIS stores | +| `Keyfactor.WinCert.SQL` | WinSQL stores | + +In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatically loads these modules from its own deployment at runtime — no pre-installation on the target server is required. JEA mode is the only mode that requires the modules to be pre-installed on the target server. See the **Just Enough Administration (JEA) Setup and Configuration** section for complete installation and setup instructions. + +### Security and Permission Considerations + +From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. + +For customers wishing to use something other than the local administrator account, the following information may be helpful: + +* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). + +* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) + +* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. + +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. + +* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: + - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. + - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. + - Create and delete temporary files. + - Execute certreq commands. + - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. + - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. + +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + +The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| ## Certificate Store Types @@ -1054,9 +1377,9 @@ the Keyfactor Command Portal 1. **Download the latest Windows Certificate Universal Orchestrator extension from GitHub.** - Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/Windows Certificate Orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/iis-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `Windows Certificate Orchestrator` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `iis-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | | Older than `11.0.0` | | | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | @@ -1075,10 +1398,10 @@ the Keyfactor Command Portal 3. **Create a new directory for the Windows Certificate Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `Windows Certificate Orchestrator`. + Create a new directory called `iis-orchestrator`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `Windows Certificate Orchestrator` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `iis-orchestrator` directory.** 5. **Restart the Universal Orchestrator service.** From fb4031a484bd9e3ec554d9038056554f4ff996e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 May 2026 16:34:12 +0000 Subject: [PATCH 20/53] docs: auto-generate README and documentation [skip ci] --- README.md | 337 ++---------------------------------------------------- 1 file changed, 7 insertions(+), 330 deletions(-) diff --git a/README.md b/README.md index d8461e49..8a3e8801 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -205,329 +205,6 @@ Copy this entire `PowerShell` folder to the target server (or to a network share On the **target server**, open an elevated PowerShell prompt (Run as Administrator) and run the following commands. Adjust the source path (`$sourcePath`) to wherever you placed the `PowerShell` folder in Step 1. ```powershell -# Set the source path to where you copied the PowerShell folder -$sourcePath = 'C:\Temp\PowerShell' - -# System module path — modules installed here are treated as fully trusted by PowerShell -$moduleBase = 'C:\Program Files\WindowsPowerShell\Modules' - -# Always install the Common module — required for all store types -Copy-Item -Path "$sourcePath\Keyfactor.WinCert.Common" ` - -Destination "$moduleBase\Keyfactor.WinCert.Common" ` - -Recurse -Force - -# Install the IIS module if this server hosts IIS certificate stores (WinIIS) -Copy-Item -Path "$sourcePath\Keyfactor.WinCert.IIS" ` - -Destination "$moduleBase\Keyfactor.WinCert.IIS" ` - -Recurse -Force - -# Install the SQL module if this server hosts SQL Server certificate stores (WinSQL) -Copy-Item -Path "$sourcePath\Keyfactor.WinCert.SQL" ` - -Destination "$moduleBase\Keyfactor.WinCert.SQL" ` - -Recurse -Force -``` - -> **Important:** Modules **must** be installed under `C:\Program Files\WindowsPowerShell\Modules\` (or another path listed in the system `$env:PSModulePath`). Modules installed outside of a trusted path will not run as fully trusted code inside a `ConstrainedLanguage` JEA session, and calls to .NET APIs will fail. - -Verify that the modules installed correctly by running: - -```powershell -Get-Module -ListAvailable | Where-Object { $_.Name -like 'Keyfactor.*' } -``` - -You should see entries for each module you installed. - ---- - -#### Step 3: Create the Audit Transcript Directory - -JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. - -```powershell -New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force -``` - -Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. - ---- - -#### Step 4: Review and Customize the Session Configuration File - -Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working location on the target server (e.g., `C:\Temp\KeyfactorWinCert.pssc`) and open it in a text editor. The key settings to review and customize are: - -**Run-As Account (choose one):** - -The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: - -- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. - - ```powershell - RunAsVirtualAccount = $true - ``` - -- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. - - ```powershell - # Comment out RunAsVirtualAccount and uncomment this line: - GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' - ``` - - To create a gMSA (run on a domain controller or with AD PowerShell module): - ```powershell - # Create the gMSA in Active Directory - New-ADServiceAccount -Name 'KeyfactorJEA' ` - -DNSHostName 'keyfactorjea.yourdomain.com' ` - -PrincipalsAllowedToRetrieveManagedPassword 'KeyfactorServers$' - - # On the target server, install the gMSA - Install-ADServiceAccount -Identity 'KeyfactorJEA$' - - # Verify the gMSA can log on - Test-ADServiceAccount -Identity 'KeyfactorJEA$' - ``` - -**Role Definitions (who is allowed to connect):** - -The `RoleDefinitions` section maps connecting users or groups to JEA role capabilities. Replace `BUILTIN\Administrators` with the specific AD group or local group whose members should be allowed to connect via JEA. Using a dedicated AD group is strongly recommended for production environments. - -```powershell -RoleDefinitions = @{ - # Replace with the AD group that contains your Keyfactor orchestrator service accounts: - 'DOMAIN\KeyfactorOrchestrators' = @{ - RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' - } -} -``` - -Only list the `RoleCapabilities` whose corresponding modules are installed on this server. The available combinations are: - -| Store Types on This Server | RoleCapabilities to List | -|---|---| -| WinCert only | `'Keyfactor.WinCert.Common'` | -| WinIIS only or WinCert + WinIIS | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS'` | -| WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | -| WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | - ---- - -#### Step 5: Register the JEA Session Configuration - -On the **target server**, in an elevated PowerShell prompt, register the session configuration. This creates the named WinRM endpoint that the Keyfactor orchestrator will connect to. - -```powershell -Register-PSSessionConfiguration ` - -Name 'keyfactor.wincert' ` - -Path 'C:\Temp\KeyfactorWinCert.pssc' ` - -Force - -# WinRM must be restarted for the new endpoint to become active -Restart-Service WinRM -``` - -> **Note:** `Restart-Service WinRM` will briefly interrupt all active WinRM connections on the server. Schedule this during a maintenance window if other services depend on WinRM. - -The name `keyfactor.wincert` is the endpoint name you will enter into the **JEA Endpoint Name** field in Keyfactor Command. You may use a different name if desired — just ensure it matches exactly when configuring the certificate store. - ---- - -#### Step 6: Verify the Registration - -Confirm the endpoint is registered and its configuration is correct: - -```powershell -# List all registered session configurations -Get-PSSessionConfiguration | Where-Object { $_.Name -eq 'keyfactor.wincert' } -``` - -You should see output showing the configuration name, PSVersion, and the path to the `.pssc` file. - -For a more thorough validation, connect to the JEA endpoint from a remote machine and verify that the Keyfactor functions are available: - -```powershell -# Connect to the JEA endpoint (run this from the Keyfactor orchestrator server or any machine with network access) -$cred = Get-Credential # Enter the orchestrator service account credentials -$s = New-PSSession -ComputerName '' ` - -Port 5985 ` - -ConfigurationName 'keyfactor.wincert' ` - -Credential $cred - -# List all commands available in the JEA session (should be limited to Keyfactor functions only) -Invoke-Command -Session $s -ScriptBlock { Get-Command } - -# Test a certificate inventory call (WinCert) -Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } - -# Test IIS inventory (if Keyfactor.WinCert.IIS is installed on the target) -Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorIISBoundCertificates } - -# Clean up the test session -Remove-PSSession $s -``` - -The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. - ---- - -#### Step 7: Configure the Certificate Store in Keyfactor Command - -Once the JEA endpoint is registered and verified on the target server, configure the certificate store in Keyfactor Command: - -1. Navigate to the certificate store you wish to manage via JEA (or create a new one). -2. In the store's **Custom Parameters** (also called **Store Properties**), locate the **JEA Endpoint Name** field. -3. Enter the name of the JEA session configuration you registered — for example, `keyfactor.wincert`. -4. Ensure the **Client Machine** is set to the target server's hostname or IP address. **Do not** use `localhost`, `LocalMachine`, or the `|LocalMachine` suffix — JEA requires a real WinRM network connection. -5. The **Server Username** and **Server Password** fields should contain the credentials of the account that is permitted to connect to the JEA endpoint (i.e., a member of the group specified in `RoleDefinitions` in the `.pssc` file). -6. Save the certificate store. - -When a job runs against this store, the orchestrator will automatically use the JEA endpoint instead of a standard WinRM administrative session. - ---- - -### Updating the JEA Configuration - -If you need to change the session configuration — for example, to add a new module or change the run-as account — update the `.pssc` file and re-register it: - -```powershell -# Re-register with the -Force flag to overwrite the existing registration -Register-PSSessionConfiguration ` - -Name 'keyfactor.wincert' ` - -Path 'C:\Temp\KeyfactorWinCert.pssc' ` - -Force - -Restart-Service WinRM -``` - -If you update one of the Keyfactor modules (e.g., after upgrading the WinCert extension), repeat Step 2 to copy the new module files to the target server. No re-registration of the session configuration is necessary for module-only updates — the next JEA session will load the updated module automatically. - ---- - -### Removing the JEA Configuration - -To remove the JEA endpoint from a server: - -```powershell -Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' -Restart-Service WinRM -``` - -After removing the endpoint, any certificate stores in Keyfactor Command that reference the JEA endpoint name will fail. Update those stores to either clear the **JEA Endpoint Name** field (to revert to standard WinRM) or point to a different JEA endpoint. - ---- - -### Troubleshooting - -**"JEA endpoint is reachable but Keyfactor modules are not installed"** - -The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). - -**"The term 'Get-KeyfactorCertificates' is not recognized..."** - -The function is not visible in the JEA session. Verify that: -- The module containing that function is installed on the target server (Step 2). -- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). -- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). -- The session configuration was re-registered and WinRM was restarted after any changes. - -**"Connecting user is not authorized to connect to this configuration"** - -The account used in the certificate store credentials is not a member of any group listed in `RoleDefinitions`. Add the account (or a group containing it) to the `RoleDefinitions` section in the `.pssc`, re-register the configuration, and restart WinRM. - -**"Access is denied" or "WinRM cannot complete the operation"** - -This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: -- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). -- The WinRM firewall rule allows connections from the orchestrator server's IP. -- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. - -**"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** - -A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. - -**Reviewing JEA Transcripts** - -All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. - -```powershell -# List recent transcript files -Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | Sort-Object LastWriteTime -Descending | Select-Object -First 10 - -# View the most recent transcript -Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 | - Get-Content -``` - ---- - -### Important Notes and Limitations - -- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. -- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. -- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. -- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. -- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. -- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. - -

- -Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. - -### PowerShell Requirements -PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). -In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. - -**JEA Module Requirements:** When using JEA (Just Enough Administration) to connect to a target server, the Keyfactor PowerShell modules must be pre-installed on each target server under `C:\Program Files\WindowsPowerShell\Modules\`. These modules are included in the WinCert extension deployment package inside the `PowerShell` folder. The modules that must be installed depend on which store types are managed on that server: - -| Module | Required For | -|---|---| -| `Keyfactor.WinCert.Common` | All store types — must always be installed | -| `Keyfactor.WinCert.IIS` | WinIIS stores | -| `Keyfactor.WinCert.SQL` | WinSQL stores | - -In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatically loads these modules from its own deployment at runtime — no pre-installation on the target server is required. JEA mode is the only mode that requires the modules to be pre-installed on the target server. See the **Just Enough Administration (JEA) Setup and Configuration** section for complete installation and setup instructions. - -### Security and Permission Considerations - -From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. - -For customers wishing to use something other than the local administrator account, the following information may be helpful: - -* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). - -* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) - -* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. - -* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. - -* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - - Create and delete temporary files. - - Execute certreq commands. - - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. - -### Using Crypto Service Providers (CSP) -When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. - -The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: - - certutil -csplist - -When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. - -When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. - -Each CSP only supports certain key types and algorithms. - -Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: -|CSP Name|Supports RSA?|Supports ECC?| -|---|---|---| -|Microsoft RSA SChannel Cryptographic Provider |✅|❌| -|Microsoft Software Key Storage Provider |✅|✅| -|Microsoft Enhanced Cryptographic Provider |✅|❌| ## Certificate Store Types @@ -1377,9 +1054,9 @@ the Keyfactor Command Portal 1. **Download the latest Windows Certificate Universal Orchestrator extension from GitHub.** - Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/iis-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/Windows Certificate Orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `iis-orchestrator` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `Windows Certificate Orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | | Older than `11.0.0` | | | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | @@ -1398,10 +1075,10 @@ the Keyfactor Command Portal 3. **Create a new directory for the Windows Certificate Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `iis-orchestrator`. + Create a new directory called `Windows Certificate Orchestrator`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `iis-orchestrator` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `Windows Certificate Orchestrator` directory.** 5. **Restart the Universal Orchestrator service.** From d9fbee87664819d24d149240a62afee42a234117 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 May 2026 17:10:44 +0000 Subject: [PATCH 21/53] docs: auto-generate README and documentation [skip ci] --- README.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) diff --git a/README.md b/README.md index 8a3e8801..63086c0c 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,329 @@ Copy this entire `PowerShell` folder to the target server (or to a network share On the **target server**, open an elevated PowerShell prompt (Run as Administrator) and run the following commands. Adjust the source path (`$sourcePath`) to wherever you placed the `PowerShell` folder in Step 1. ```powershell +# Set the source path to where you copied the PowerShell folder +$sourcePath = 'C:\Temp\PowerShell' + +# System module path — modules installed here are treated as fully trusted by PowerShell +$moduleBase = 'C:\Program Files\WindowsPowerShell\Modules' + +# Always install the Common module — required for all store types +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.Common" ` + -Destination "$moduleBase\Keyfactor.WinCert.Common" ` + -Recurse -Force + +# Install the IIS module if this server hosts IIS certificate stores (WinIIS) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.IIS" ` + -Destination "$moduleBase\Keyfactor.WinCert.IIS" ` + -Recurse -Force + +# Install the SQL module if this server hosts SQL Server certificate stores (WinSQL) +Copy-Item -Path "$sourcePath\Keyfactor.WinCert.SQL" ` + -Destination "$moduleBase\Keyfactor.WinCert.SQL" ` + -Recurse -Force +``` + +> **Important:** Modules **must** be installed under `C:\Program Files\WindowsPowerShell\Modules\` (or another path listed in the system `$env:PSModulePath`). Modules installed outside of a trusted path will not run as fully trusted code inside a `ConstrainedLanguage` JEA session, and calls to .NET APIs will fail. + +Verify that the modules installed correctly by running: + +```powershell +Get-Module -ListAvailable | Where-Object { $_.Name -like 'Keyfactor.*' } +``` + +You should see entries for each module you installed. + +--- + +#### Step 3: Create the Audit Transcript Directory + +JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. + +```powershell +New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force +``` + +Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. + +--- + +#### Step 4: Review and Customize the Session Configuration File + +Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working location on the target server (e.g., `C:\Temp\KeyfactorWinCert.pssc`) and open it in a text editor. The key settings to review and customize are: + +**Run-As Account (choose one):** + +The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: + +- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. + + ```powershell + RunAsVirtualAccount = $true + ``` + +- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. + + ```powershell + # Comment out RunAsVirtualAccount and uncomment this line: + GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' + ``` + + To create a gMSA (run on a domain controller or with AD PowerShell module): + ```powershell + # Create the gMSA in Active Directory + New-ADServiceAccount -Name 'KeyfactorJEA' ` + -DNSHostName 'keyfactorjea.yourdomain.com' ` + -PrincipalsAllowedToRetrieveManagedPassword 'KeyfactorServers$' + + # On the target server, install the gMSA + Install-ADServiceAccount -Identity 'KeyfactorJEA$' + + # Verify the gMSA can log on + Test-ADServiceAccount -Identity 'KeyfactorJEA$' + ``` + +**Role Definitions (who is allowed to connect):** + +The `RoleDefinitions` section maps connecting users or groups to JEA role capabilities. Replace `BUILTIN\Administrators` with the specific AD group or local group whose members should be allowed to connect via JEA. Using a dedicated AD group is strongly recommended for production environments. + +```powershell +RoleDefinitions = @{ + # Replace with the AD group that contains your Keyfactor orchestrator service accounts: + 'DOMAIN\KeyfactorOrchestrators' = @{ + RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' + } +} +``` + +Only list the `RoleCapabilities` whose corresponding modules are installed on this server. The available combinations are: + +| Store Types on This Server | RoleCapabilities to List | +|---|---| +| WinCert only | `'Keyfactor.WinCert.Common'` | +| WinIIS only or WinCert + WinIIS | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS'` | +| WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | +| WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | + +--- + +#### Step 5: Register the JEA Session Configuration + +On the **target server**, in an elevated PowerShell prompt, register the session configuration. This creates the named WinRM endpoint that the Keyfactor orchestrator will connect to. + +```powershell +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +# WinRM must be restarted for the new endpoint to become active +Restart-Service WinRM +``` + +> **Note:** `Restart-Service WinRM` will briefly interrupt all active WinRM connections on the server. Schedule this during a maintenance window if other services depend on WinRM. + +The name `keyfactor.wincert` is the endpoint name you will enter into the **JEA Endpoint Name** field in Keyfactor Command. You may use a different name if desired — just ensure it matches exactly when configuring the certificate store. + +--- + +#### Step 6: Verify the Registration + +Confirm the endpoint is registered and its configuration is correct: + +```powershell +# List all registered session configurations +Get-PSSessionConfiguration | Where-Object { $_.Name -eq 'keyfactor.wincert' } +``` + +You should see output showing the configuration name, PSVersion, and the path to the `.pssc` file. + +For a more thorough validation, connect to the JEA endpoint from a remote machine and verify that the Keyfactor functions are available: + +```powershell +# Connect to the JEA endpoint (run this from the Keyfactor orchestrator server or any machine with network access) +$cred = Get-Credential # Enter the orchestrator service account credentials +$s = New-PSSession -ComputerName '' ` + -Port 5985 ` + -ConfigurationName 'keyfactor.wincert' ` + -Credential $cred + +# List all commands available in the JEA session (should be limited to Keyfactor functions only) +Invoke-Command -Session $s -ScriptBlock { Get-Command } + +# Test a certificate inventory call (WinCert) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } + +# Test IIS inventory (if Keyfactor.WinCert.IIS is installed on the target) +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorIISBoundCertificates } + +# Clean up the test session +Remove-PSSession $s +``` + +The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. + +--- + +#### Step 7: Configure the Certificate Store in Keyfactor Command + +Once the JEA endpoint is registered and verified on the target server, configure the certificate store in Keyfactor Command: + +1. Navigate to the certificate store you wish to manage via JEA (or create a new one). +2. In the store's **Custom Parameters** (also called **Store Properties**), locate the **JEA Endpoint Name** field. +3. Enter the name of the JEA session configuration you registered — for example, `keyfactor.wincert`. +4. Ensure the **Client Machine** is set to the target server's hostname or IP address. **Do not** use `localhost`, `LocalMachine`, or the `|LocalMachine` suffix — JEA requires a real WinRM network connection. +5. The **Server Username** and **Server Password** fields should contain the credentials of the account that is permitted to connect to the JEA endpoint (i.e., a member of the group specified in `RoleDefinitions` in the `.pssc` file). +6. Save the certificate store. + +When a job runs against this store, the orchestrator will automatically use the JEA endpoint instead of a standard WinRM administrative session. + +--- + +### Updating the JEA Configuration + +If you need to change the session configuration — for example, to add a new module or change the run-as account — update the `.pssc` file and re-register it: + +```powershell +# Re-register with the -Force flag to overwrite the existing registration +Register-PSSessionConfiguration ` + -Name 'keyfactor.wincert' ` + -Path 'C:\Temp\KeyfactorWinCert.pssc' ` + -Force + +Restart-Service WinRM +``` + +If you update one of the Keyfactor modules (e.g., after upgrading the WinCert extension), repeat Step 2 to copy the new module files to the target server. No re-registration of the session configuration is necessary for module-only updates — the next JEA session will load the updated module automatically. + +--- + +### Removing the JEA Configuration + +To remove the JEA endpoint from a server: + +```powershell +Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' +Restart-Service WinRM +``` + +After removing the endpoint, any certificate stores in Keyfactor Command that reference the JEA endpoint name will fail. Update those stores to either clear the **JEA Endpoint Name** field (to revert to standard WinRM) or point to a different JEA endpoint. + +--- + +### Troubleshooting + +**"JEA endpoint is reachable but Keyfactor modules are not installed"** + +The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). + +**"The term 'Get-KeyfactorCertificates' is not recognized..."** + +The function is not visible in the JEA session. Verify that: +- The module containing that function is installed on the target server (Step 2). +- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). +- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). +- The session configuration was re-registered and WinRM was restarted after any changes. + +**"Connecting user is not authorized to connect to this configuration"** + +The account used in the certificate store credentials is not a member of any group listed in `RoleDefinitions`. Add the account (or a group containing it) to the `RoleDefinitions` section in the `.pssc`, re-register the configuration, and restart WinRM. + +**"Access is denied" or "WinRM cannot complete the operation"** + +This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: +- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). +- The WinRM firewall rule allows connections from the orchestrator server's IP. +- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. + +**"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** + +A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. + +**Reviewing JEA Transcripts** + +All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. + +```powershell +# List recent transcript files +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | Sort-Object LastWriteTime -Descending | Select-Object -First 10 + +# View the most recent transcript +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content +``` + +--- + +### Important Notes and Limitations + +- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. +- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. +- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. +- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. +- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. +- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. + +
+ +Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. + +### PowerShell Requirements +PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). +In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. + +**JEA Module Requirements:** When using JEA (Just Enough Administration) to connect to a target server, the Keyfactor PowerShell modules must be pre-installed on each target server under `C:\Program Files\WindowsPowerShell\Modules\`. These modules are included in the WinCert extension deployment package inside the `PowerShell` folder. The modules that must be installed depend on which store types are managed on that server: + +| Module | Required For | +|---|---| +| `Keyfactor.WinCert.Common` | All store types — must always be installed | +| `Keyfactor.WinCert.IIS` | WinIIS stores | +| `Keyfactor.WinCert.SQL` | WinSQL stores | + +In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatically loads these modules from its own deployment at runtime — no pre-installation on the target server is required. JEA mode is the only mode that requires the modules to be pre-installed on the target server. See the **Just Enough Administration (JEA) Setup and Configuration** section for complete installation and setup instructions. + +### Security and Permission Considerations + +From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. + +For customers wishing to use something other than the local administrator account, the following information may be helpful: + +* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). + +* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) + +* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. + +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. + +* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: + - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. + - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. + - Create and delete temporary files. + - Execute certreq commands. + - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. + - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. + +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + +The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| ## Certificate Store Types From 89f92f05f9df26816eec4ffbeffbfb0ac60b324f Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 07:11:39 -0700 Subject: [PATCH 22/53] Adding dotnet 10 support --- IISU/WindowsCertStore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index d8b94155..c4c77a82 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -3,7 +3,7 @@ Keyfactor.Extensions.Orchestrator.WindowsCertStore true - net6.0;net8.0 + net6.0;net8.0;net10.0 AnyCPU disable From 0413000e87f71503d0adbf9595158d87bb97ae48 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 09:18:26 -0500 Subject: [PATCH 23/53] modified: IISU/PowerShell/Build/KeyfactorWinCert.pssc modified: docsource/content.md --- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 14 +++++-- docsource/content.md | 45 ++++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc index 5dc01f51..c600a4d2 100644 --- a/IISU/PowerShell/Build/KeyfactorWinCert.pssc +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -15,7 +15,8 @@ # # (Only install the modules needed for the store types you use on this endpoint.) # -# 2. Create the transcript directory for audit logging: +# 2. (Optional) Create the transcript directory for audit logging. +# Only required if TranscriptDirectory is uncommented in the session configuration below. # # New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force # @@ -79,9 +80,14 @@ # RunAsVirtualAccount = $true - # Transcript every session to this directory for audit purposes. - # Directory must exist before registering -- see Prerequisites step 2 above. - TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' + # --- Transcript Logging (Optional) --- + # Uncomment TranscriptDirectory to record a full transcript of every JEA session. + # Useful during initial testing and for ongoing security audits. + # The directory must exist before registering the configuration -- create it with: + # New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force + # When commented out, no transcripts are written. + # + # TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' # --- Role Definitions --- # Map the connecting user/group to one or more Keyfactor role capabilities. diff --git a/docsource/content.md b/docsource/content.md index 820979c1..10b581b2 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -189,15 +189,32 @@ You should see entries for each module you installed. --- -#### Step 3: Create the Audit Transcript Directory +#### Step 3: (Optional) Create the Audit Transcript Directory -JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. +Transcript logging is **disabled by default** in the session configuration file. When enabled, JEA records a full transcript of every session — every function called, with its parameters and output — to a directory on the target server. This is highly recommended while you are first testing the JEA setup, and may be required by your organization's security policy in production. + +To enable transcription, you must do two things: create the directory (this step), and uncomment the `TranscriptDirectory` line in the `.pssc` file (covered in Step 4). ```powershell New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force ``` -Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. +Each transcript file is named with the date, time, and a unique identifier so sessions are never overwritten. To review recent transcripts: + +```powershell +# List the 10 most recent transcript files +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 10 + +# View the most recent transcript +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content +``` + +If you choose not to enable transcript logging, skip this step entirely — no directory is needed when `TranscriptDirectory` remains commented out in the `.pssc`. --- @@ -258,6 +275,24 @@ Only list the `RoleCapabilities` whose corresponding modules are installed on th | WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | | WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | +**Transcript Logging (Optional):** + +The `TranscriptDirectory` setting in the `.pssc` file is **commented out by default**. When commented out, no transcript files are written and the directory created in Step 3 is not needed. This is a reasonable choice for production environments where the volume of orchestrator activity would generate a large number of transcript files, or where audit logging is handled by another mechanism (e.g., WinRM event logs or a SIEM). + +To enable transcript logging, locate the `TranscriptDirectory` line in the `.pssc` file and remove the `#` comment character: + +```powershell +# Before (transcription disabled — default): +# TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' + +# After (transcription enabled): +TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' +``` + +> **Recommendation:** Enable transcript logging during initial setup and testing. It makes it easy to confirm that the orchestrator is calling the correct functions with the correct parameters, and to diagnose any unexpected failures. Once you are confident the configuration is working correctly in production, you may choose to disable it to reduce disk usage — or keep it enabled to satisfy your organization's audit requirements. + +> **Important:** If you enable `TranscriptDirectory`, you must also create the directory before registering the session configuration (Step 3). If the directory does not exist at registration time, `Register-PSSessionConfiguration` will fail. + --- #### Step 5: Register the JEA Session Configuration @@ -393,9 +428,9 @@ This typically indicates a WinRM connectivity issue rather than a JEA-specific p A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. -**Reviewing JEA Transcripts** +**Reviewing JEA Transcripts (if transcript logging is enabled)** -All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. +If `TranscriptDirectory` is uncommented in the `.pssc` file, JEA writes a full transcript of every session to that directory on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. See Steps 3 and 4 for instructions on enabling this feature. ```powershell # List recent transcript files From 6395a2364877a8a1b2b038838832e7a975f37c8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 14:20:03 +0000 Subject: [PATCH 24/53] docs: auto-generate README and documentation [skip ci] --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 63086c0c..fa6ac41b 100644 --- a/README.md +++ b/README.md @@ -239,15 +239,32 @@ You should see entries for each module you installed. --- -#### Step 3: Create the Audit Transcript Directory +#### Step 3: (Optional) Create the Audit Transcript Directory -JEA records a full transcript of every session for audit purposes. The transcript directory must exist before you register the session configuration. +Transcript logging is **disabled by default** in the session configuration file. When enabled, JEA records a full transcript of every session — every function called, with its parameters and output — to a directory on the target server. This is highly recommended while you are first testing the JEA setup, and may be required by your organization's security policy in production. + +To enable transcription, you must do two things: create the directory (this step), and uncomment the `TranscriptDirectory` line in the `.pssc` file (covered in Step 4). ```powershell New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force ``` -Transcripts are written here automatically for every connection made through the JEA endpoint. Review these files periodically to audit orchestrator activity. Each transcript file is named with the date, time, and a unique identifier so that sessions are never overwritten. +Each transcript file is named with the date, time, and a unique identifier so sessions are never overwritten. To review recent transcripts: + +```powershell +# List the 10 most recent transcript files +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 10 + +# View the most recent transcript +Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content +``` + +If you choose not to enable transcript logging, skip this step entirely — no directory is needed when `TranscriptDirectory` remains commented out in the `.pssc`. --- @@ -308,6 +325,24 @@ Only list the `RoleCapabilities` whose corresponding modules are installed on th | WinSQL only or WinCert + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL'` | | WinCert + WinIIS + WinSQL | `'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL'` | +**Transcript Logging (Optional):** + +The `TranscriptDirectory` setting in the `.pssc` file is **commented out by default**. When commented out, no transcript files are written and the directory created in Step 3 is not needed. This is a reasonable choice for production environments where the volume of orchestrator activity would generate a large number of transcript files, or where audit logging is handled by another mechanism (e.g., WinRM event logs or a SIEM). + +To enable transcript logging, locate the `TranscriptDirectory` line in the `.pssc` file and remove the `#` comment character: + +```powershell +# Before (transcription disabled — default): +# TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' + +# After (transcription enabled): +TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' +``` + +> **Recommendation:** Enable transcript logging during initial setup and testing. It makes it easy to confirm that the orchestrator is calling the correct functions with the correct parameters, and to diagnose any unexpected failures. Once you are confident the configuration is working correctly in production, you may choose to disable it to reduce disk usage — or keep it enabled to satisfy your organization's audit requirements. + +> **Important:** If you enable `TranscriptDirectory`, you must also create the directory before registering the session configuration (Step 3). If the directory does not exist at registration time, `Register-PSSessionConfiguration` will fail. + --- #### Step 5: Register the JEA Session Configuration @@ -443,9 +478,9 @@ This typically indicates a WinRM connectivity issue rather than a JEA-specific p A **JEA Endpoint Name** was entered in the certificate store but the **Client Machine** is set to `localhost`, `LocalMachine`, or uses the `|LocalMachine` suffix. JEA is not compatible with local-machine (agent) mode. Either remove the JEA endpoint name to use direct local access, or change the Client Machine to the server's actual hostname or IP address to use JEA over WinRM. -**Reviewing JEA Transcripts** +**Reviewing JEA Transcripts (if transcript logging is enabled)** -All JEA sessions are transcribed to `C:\ProgramData\Keyfactor\JEA\Transcripts\` on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. +If `TranscriptDirectory` is uncommented in the `.pssc` file, JEA writes a full transcript of every session to that directory on the target server. Each transcript file records the session start time, the connecting user, all commands executed (including parameter values), and the session end time. These files are invaluable for diagnosing job failures and for security audits. See Steps 3 and 4 for instructions on enabling this feature. ```powershell # List recent transcript files From aa8d50454c5cc804048ac99fd9353e0478fa93a8 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 09:36:47 -0500 Subject: [PATCH 25/53] Updated packages to support dotnet 10 --- IISU/WindowsCertStore.csproj | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index c4c77a82..89798d0f 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -31,19 +31,21 @@ - - - - + + + + + - + + From 43035b7b9ce7005714a9672ed280608f5fd101c5 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 09:53:37 -0700 Subject: [PATCH 26/53] Updating Unit Tests --- IISU/ClientPSCertStoreReEnrollment.cs | 2 +- .../WinAdfs/AdfsCertificateRotationManager.cs | 2 +- .../ClientConnection.cs | 6 ++--- .../Factories/CertificateFactory.cs | 4 +-- .../WinCertIntegrationTests.cs | 2 +- .../WinIISIntegrationTests.cs | 2 +- WindowsCertStore.UnitTests/SANsUnitTests.cs | 25 ++++++++++++++++--- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/IISU/ClientPSCertStoreReEnrollment.cs b/IISU/ClientPSCertStoreReEnrollment.cs index bdcaa309..1c7cc02c 100644 --- a/IISU/ClientPSCertStoreReEnrollment.cs +++ b/IISU/ClientPSCertStoreReEnrollment.cs @@ -398,7 +398,7 @@ public string ResolveSANString(ReenrollmentJobConfiguration config) } else if (config.JobProperties != null && config.JobProperties.TryGetValue("SAN", out object legacySanValue) && - !string.IsNullOrWhiteSpace(legacySanValue.ToString())) + (legacySanValue is not null && !string.IsNullOrWhiteSpace(legacySanValue.ToString()))) { sanValue = legacySanValue.ToString().Trim(); sourceUsed = "config.JobProperties[\"SAN\"] (legacy)"; diff --git a/IISU/ImplementedStoreTypes/WinAdfs/AdfsCertificateRotationManager.cs b/IISU/ImplementedStoreTypes/WinAdfs/AdfsCertificateRotationManager.cs index 982477e1..ad2ceaf9 100644 --- a/IISU/ImplementedStoreTypes/WinAdfs/AdfsCertificateRotationManager.cs +++ b/IISU/ImplementedStoreTypes/WinAdfs/AdfsCertificateRotationManager.cs @@ -553,7 +553,7 @@ public static void UpdateFarmCertificateSettings(string thumbprint, PSHelper psH } } } - catch (Exception ex) + catch (Exception) { throw; } diff --git a/WindowsCertStore.IntegrationTests/ClientConnection.cs b/WindowsCertStore.IntegrationTests/ClientConnection.cs index 47d074b4..50eae5e4 100644 --- a/WindowsCertStore.IntegrationTests/ClientConnection.cs +++ b/WindowsCertStore.IntegrationTests/ClientConnection.cs @@ -8,10 +8,10 @@ namespace WindowsCertStore.IntegrationTests { public class ClientConnection { - public string Machine { get; set; } + public string? Machine { get; set; } public string StoreType { get; set; } = ""; public string JEAEndpointName { get; set; } = ""; - public string Username { get; set; } - public string PrivateKey { get; set; } + public string? Username { get; set; } + public string? PrivateKey { get; set; } } } diff --git a/WindowsCertStore.IntegrationTests/Factories/CertificateFactory.cs b/WindowsCertStore.IntegrationTests/Factories/CertificateFactory.cs index e6e476bb..6ddbb1c7 100644 --- a/WindowsCertStore.IntegrationTests/Factories/CertificateFactory.cs +++ b/WindowsCertStore.IntegrationTests/Factories/CertificateFactory.cs @@ -17,7 +17,7 @@ public static class CertificateFactory /// The subject name for the certificate (CN=...) /// The password to protect the PFX file /// Tuple of Thumbprint and Base64 PFX string - public static (string Thumbprint, string Base64Pfx) CreateSelfSignedCert(string subjectName, string pfxPassword) + public static (string? Thumbprint, string? Base64Pfx) CreateSelfSignedCert(string subjectName, string pfxPassword) { using (RSA rsa = RSA.Create(2048)) { @@ -51,7 +51,7 @@ public static (string Thumbprint, string Base64Pfx) CreateSelfSignedCert(string string base64Pfx = Convert.ToBase64String(pfxBytes); // Thumbprint (uppercase, no spaces) - string thumbprint = cert.Thumbprint?.Replace(" ", "").ToUpperInvariant(); + string? thumbprint = cert.Thumbprint?.Replace(" ", "").ToUpperInvariant(); return (thumbprint, base64Pfx); } diff --git a/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs b/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs index e789dc13..5309a52c 100644 --- a/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs +++ b/WindowsCertStore.IntegrationTests/WinCertIntegrationTests.cs @@ -19,7 +19,7 @@ private static (string thumbprint, string base64Pfx, string pfxPassword) CreateT private static ManagementJobConfiguration CreateManagementJobConfig( ClientConnection connection, - string thumbprint, + string? thumbprint, string base64Pfx, string pfxPassword, string alias, diff --git a/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs b/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs index 38ee6dff..3e9ba81c 100644 --- a/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs +++ b/WindowsCertStore.IntegrationTests/WinIISIntegrationTests.cs @@ -20,7 +20,7 @@ private static (string thumbprint, string base64Pfx, string pfxPassword) CreateT private static ManagementJobConfiguration CreateManagementJobConfig( ClientConnection connection, - string thumbprint, + string? thumbprint, string base64Pfx, string pfxPassword, string alias, diff --git a/WindowsCertStore.UnitTests/SANsUnitTests.cs b/WindowsCertStore.UnitTests/SANsUnitTests.cs index 44ca0056..e193ffe4 100644 --- a/WindowsCertStore.UnitTests/SANsUnitTests.cs +++ b/WindowsCertStore.UnitTests/SANsUnitTests.cs @@ -59,15 +59,18 @@ public void ResolveSanString_PrefersConfigSANs_WhenBothSourcesExist() Assert.DoesNotContain("legacy.example.com", result); // ensure legacy ignored } - [Fact] - public void ResolveSanString_UsesLegacySAN_WhenConfigSANsMissing() + [Theory] + [InlineData("dns=legacy.example.com&dns=old.example.com")] + [InlineData("")] + [InlineData(null)] + public void ResolveSanString_UsesLegacySAN_WhenConfigSANsMissing(string legacySan) { // Arrange var config = new ReenrollmentJobConfiguration { JobProperties = new Dictionary { - { "SAN", "dns=legacy.example.com&dns=old.example.com" } + { "SAN", legacySan } }, SANs = new Dictionary() }; @@ -76,7 +79,21 @@ public void ResolveSanString_UsesLegacySAN_WhenConfigSANsMissing() string result = enrollment.ResolveSANString(config); // Assert - Assert.Equal("dns=legacy.example.com&dns=old.example.com", result); + //Assert.Equal(legacySan, result); + + // Assert + if (legacySan == null) + { + Assert.Equal(string.Empty, result); + } + else if (legacySan == string.Empty) + { + Assert.Equal(string.Empty, result); + } + else + { + Assert.Equal(legacySan, result); + } } [Fact] From 9197a2c25efc64540dc7d739c8507ef7998582bb Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 12:17:31 -0500 Subject: [PATCH 27/53] Fixed timeout issue when migrating to dotnet 10 --- IISU/PSHelper.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index c788732a..45beaa4b 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -192,6 +192,8 @@ private void InitializeRemoteSession() if (this.isADFSStore) throw new Exception("Remote ADFS stores are not supported."); if (this.useJea && protocol == "ssh") throw new Exception("JEA is not supported over SSH. Use WinRM (http/https) for JEA."); + double timeoutSeconds = 30.0; + if (protocol == "ssh") { _logger.LogTrace("Initializing SSH connection"); @@ -228,7 +230,8 @@ private void InitializeRemoteSession() // Create the PSSessionOption object var sessionOption = new PSSessionOption { - IncludePortInSPN = useSPN + IncludePortInSPN = useSPN, + OpenTimeout = TimeSpan.FromSeconds(timeoutSeconds) }; PS.AddCommand("New-PSSession") @@ -264,10 +267,17 @@ private void InitializeRemoteSession() } - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - _logger.LogTrace("Attempting to invoke PS-Session command on remote machine."); - _PSSession = PS.Invoke(); + + var asyncResult = PS.BeginInvoke(); + if (!asyncResult.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(timeoutSeconds))) + { + PS.Stop(); + throw new TimeoutException( + $"Could not establish a remote PowerShell session to '{machineName}:{port}' within {timeoutSeconds} seconds. " + + "Verify WinRM is reachable and the firewall allows the connection."); + } + _PSSession = new Collection(PS.EndInvoke(asyncResult)); if (_PSSession.Count > 0) { From a85707dfd9141b4ba4b3d29c7c5a9eeaa9713654 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 14:47:24 -0500 Subject: [PATCH 28/53] modified: IISU/PSHelper.cs --- IISU/PSHelper.cs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 45beaa4b..9b7ad03d 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -227,17 +227,15 @@ private void InitializeRemoteSession() _logger.LogTrace("Initializing WinRM connection"); try { - // Create the PSSessionOption object - var sessionOption = new PSSessionOption - { - IncludePortInSPN = useSPN, - OpenTimeout = TimeSpan.FromSeconds(timeoutSeconds) - }; - PS.AddCommand("New-PSSession") - .AddParameter("ComputerName", ClientMachineName) - .AddParameter("Port", port) - .AddParameter("SessionOption", sessionOption); + .AddParameter("ComputerName", ClientMachineName) + .AddParameter("Port", port); + + if (useSPN) + { + var sessionOption = new PSSessionOption { IncludePortInSPN = true }; + PS.AddParameter("SessionOption", sessionOption); + } if (useJea) { @@ -260,8 +258,9 @@ private void InitializeRemoteSession() } } - catch (Exception) + catch (Exception ex) { + _logger.LogError($"An error occurred while attempting to establish a remote connection.\n {ex.Message}"); throw new Exception("Problems establishing network credentials. Please check the User name and Password for the Certificate Store"); } From fb7444c3c303c1dc45d155a509723e448175c6f0 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 13 May 2026 15:18:13 -0500 Subject: [PATCH 29/53] Remove .net6 libraries and support --- IISU/WindowsCertStore.csproj | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index 89798d0f..25ebe0f2 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -3,7 +3,7 @@ Keyfactor.Extensions.Orchestrator.WindowsCertStore true - net6.0;net8.0;net10.0 + net8.0;net10.0 AnyCPU disable @@ -34,18 +34,13 @@ - - - - - From 6b3a95ae91b12c6cdcfededcc636152f2b94a136 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 20:18:56 +0000 Subject: [PATCH 30/53] docs: auto-generate README and documentation [skip ci] --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index fa6ac41b..b9f58d0a 100644 --- a/README.md +++ b/README.md @@ -1416,15 +1416,12 @@ the Keyfactor Command Portal | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `Windows Certificate Orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | - | Older than `11.0.0` | | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. - > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`. + > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net8.0`. 2. **Locate the Universal Orchestrator extensions directory.** From 467ed398567adc2baaf9d3be42a8de9ece8e0dbb Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 15 May 2026 06:10:55 -0700 Subject: [PATCH 31/53] #86137 Cleaned up messages returned to Command --- CHANGELOG.md | 103 +++++++++----- IISU/ImplementedStoreTypes/Win/Inventory.cs | 2 +- IISU/ImplementedStoreTypes/Win/Management.cs | 2 +- .../WinAdfs/Management.cs | 2 +- .../ImplementedStoreTypes/WinSQL/Inventory.cs | 2 +- .../WinSQL/Management.cs | 2 +- IISU/PSHelper.cs | 134 ------------------ 7 files changed, 73 insertions(+), 174 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632a987b..47ac27af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ 4.0.0 + * As of this version of the extension, SANs will be handled through the ODKG Enrollment page in Command and will no longer use the SAN Entry Parameter. This version, we are removing all support for the SAN Entry Parameter. If you are still using the SAN Entry Parameter, you will need to remove it from your store types and re-run inventory to remove it from your database. * Adding JEA Support for local PowerShell execution. This will allow for more secure execution of the extension when running in a local PowerShell Runspace. To utilize this feature, you will need to create a JEA endpoint on the target server and specify the endpoint name as a new parameter in the specific Cert Store definition. Refer to the README for more details. - +* .NET6 assemblies are no longer supported. + 3.0.1 -* Fixed an issues when renewing ECC Certificates +* Fixed an issues when renewing ECC Certificates + 3.0.0 + * As of this version of the extension, SANs will be handled through the ODKG Enrollment page in Command, and will no longer use the SAN Entry Parameter. This version, we are removing the Entry Parameter "SAN" from the integration-manifest.json, but will still support previous versions of Command in the event the SAN Entry Parameter is passed. The next major version (4.0) will remove all support for the SAN Entry Parameter. * Added WinADFS Store Type for rotating certificates in ADFS environments. Please note, only the service-communications certificate is rotated throughout your farm. * Internal only: Added Integration Tests to aid in future development and testing. @@ -13,38 +17,45 @@ * Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags * Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags * Added SSL Flag validation to make sure the bit flag is correct. These are the valid bit flags for the version of Windows: - ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): - * 0 No SNI - * 1 Use SNI - * 2 Use Centralized SSL certificate store. - - ### Windows Server 2016 (IIS 10.0): - * 0 No SNI - * 1 Use SNI - * 4 Disable HTTP/2. - - ### Windows Server 2019 (IIS 10.0.17763) - * 0 No SNI - * 1 Use SNI - * 4 Disable HTTP/2. - * 8 Disable OCSP Stapling. - - ### Windows Server 2022+ (IIS 10.0.20348+) - * 0 No SNI - * 1 Use SNI - * 4 Disable HTTP/2. - * 8 Disable OCSP Stapling. - * 16 Disable QUIC. - * 32 Disable TLS 1.3 over TCP. - * 64 Disable Legacy TLS. + ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): + + * 0 No SNI + + * 1 Use SNI + * 2 Use Centralized SSL certificate store. + + ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): + * 0 No SNI + + * 1 Use SNI + * 4 Disable HTTP/2. + +### Windows Server 2019 (IIS 10.0.17763) + * 0 No SNI + + * 1 Use SNI + * 4 Disable HTTP/2. + * 8 Disable OCSP Stapling. + +### Windows Server 2022+ (IIS 10.0.20348+) + * 0 No SNI + + * 1 Use SNI + * 4 Disable HTTP/2. + * 8 Disable OCSP Stapling. + * 16 Disable QUIC. + * 32 Disable TLS 1.3 over TCP. + * 64 Disable Legacy TLS. 2.6.4 + * Fixed an issue with SSL Flags greater than 3 were not being applied correctly to newer IIS servers. * Fixed an issue when formatting private RSA keys when connecting using the ssh protocol. * When using ssh protocol in containers, the SQL ACL on private keys was not being updating correctly. This has been fixed. * Updated documentation to indicate that the username and password fields on the Cert Store are automatically added by Command. 2.6.3 + * Fixed re-enrollment or ODKG job when RDN Components contained escaped commas. * Updated renewal job for IIS Certs to delete the old cert if not bound or used by other web sites. * Improved Inventory reporting of CSP when cert uses newer CNG Keys. @@ -55,23 +66,25 @@ * Fixed an issue with (remote) ODKG jobs that caused an error when the CSP was not specified that did not require binding. 2.6.2 + * Fixed error when attempting to connect to remote computer using UO service account * Fixed error when connecting to remote computer using HTTPS; was defaulting to HTTP * Fixed the creation of a certificate when the Cryptographic Service Provider was changed by the user * Updated logic when getting the CSP. Now supports modern CHG and legacy CAPI APIs. This will allow the CSP to show in the stores inventory. * Re-factored code to eliminate warnings * Bumped up he following packages to eliminate .net vulnerabilities and obsolete packages: - * Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="1.0.0" + * Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="1.0.0" * Microsoft.PowerShell.SDK" Version="7.4.10" Condition="'$(TargetFramework)' == 'net8.0'" * runtime.linux-arm64.runtime.native.System.IO.Ports" Version="9.0.5" * runtime.osx-arm64.runtime.native.System.IO.Ports" Version="9.0.5" * System.Formats.Asn1" Version="8.0.2" Condition="'$(TargetFramework)' == 'net6.0'" - * System.Formats.Asn1" Version="9.0.0" Condition="'$(TargetFramework)' == 'net8.0'" + * System.Formats.Asn1" Version="9.0.0" Condition="'$(TargetFramework)' == 'net8.0'" * System.IO.Packaging" Version="6.0.2" Condition="'$(TargetFramework)' == 'net6.0'" * System.IO.Packaging" Version="8.0.1" Condition="'$(TargetFramework)' == 'net8.0'" * System.Text.Json" Version="8.0.5" 2.6.1 + * Documentation updates for the 2.6 release * Fix a naming typo in the 2.5 migration SQL script * Update integration-manifest.json @@ -81,6 +94,7 @@ * Bumped System.IO.Packaging to 6.0.2 & 8.0.1 for .Net vulnerabilities. 2.6.0 + * Added the ability to run the extension in a Linux environment. To utilize this change, for each Cert Store Types (WinCert/WinIIS/WinSQL), add ssh to the Custom Field WinRM Protocol. When using ssh as a protocol, make sure to enter the appropriate ssh port number under WinRM Port. * NOTE: For legacy purposes the Display names WinRM Protocol and WinRM Port are maintained although the type of protocols now includes ssh. * Moved all inventory and management jobs to external PowerShell script file .\PowerShellScripts\WinCertScripts.ps1 @@ -88,9 +102,11 @@ * NOTE: This version was not publicly released. 2.5.1 + * Fixed WinSQL service name when InstanceID differs from InstanceName 2.5.0 + * Added the Bindings to the end of the thumbprint to make the alias unique. * Using new IISWebBindings cmdlet to use additional SSL flags when binding certificate to website. * NOTE: The property SNIFlag has changed from a multi-select to a string with default of "0". To properly use the new SNI/SSL flags you can delete the SNIFlag from the store type and re-add the field as described in the ReadMe. If you have several existing cert stores, you may can execute the SQL script (IISU Sni Flag 2.5 upgrade script) to update the field type. Consult your Keyfactor Rep for help. @@ -100,22 +116,27 @@ * Removed renewal thumbprint logic to update multiple website; each job now updates its own specific certificate. 2.4.4 + * Fix an issue with WinRM parameters when migrating Legacy IIS Stores to the WinCert type * Fix an issue with "Delete" script in the Legacy IIS Migration that did not remove some records from dependent tables 2.4.3 + * Adding Legacy IIS Migration scripting and ReadMe guide 2.4.2 + * Correct false positive error when completing an IIS inventory job. * Revert to specifying the version of PowerShell to use when establishing a local PowerShell Runspace. * Fixed typo in error message. 2.4.1 + * Modified the CertUtil logic to use the -addstore argument when no password is sent with the certificate information. * Added additional error trapping and trace logs 2.4.0 + * Changed the way certificates are added to cert stores. CertUtil is now used to import the PFX certificate into the associated store. The CSP is now considered when maintaining certificates, empty CSP values will result in using the machines default CSP. * Added the Crypto Service Provider and SAN Entry Parameters to be used on Inventory queries, Adding and ReEnrollments for the WinCert, WinSQL and IISU extensions. * Changed how Client Machine Names are handled when a 'localhost' connection is desired. The new naming convention is: {machineName}|localmachine. This will eliminate the issue of unique naming conflicts. @@ -123,66 +144,78 @@ * Updated the integration-manifest.json file for new fields in cert store types. 2.3.2 + * Changed the Open Cert Store access level from a '5' to 'MaxAllowed' 2.3.1 + * Added additional error trapping for WinRM connections to allow actual error on failure. 2.3.0 + * Added Sql Server Binding Support * Modified WinCert Advanced PrivateKeyAllowed setting from Required to Optional 2.2.2 + * Removed empty constructor to resolve PAM provider error when using WinCert store types 2.2.1 + * Fixed issue where https binding without cert was causing an error 2.2.0 -* Added Support for GMSA Account by using no value for ServerUsernanme and ServerPassword. KF Command version 10.2 or later is required to specify empty credentials. +* Added Support for GMSA Account by using no value for ServerUsernanme and ServerPassword. KF Command version 10.2 or later is required to specify empty credentials. + * Added local PowerShell support, triggered when specifying 'localhost' as the client machine while using the IISU or WinCert Orchestrator. This change was tested using KF Command 10.3 * Moved to .NET 6 2.1.1 + * Fixed the missing site name error when issuing a WinCert job when writing trace log settings to the log file. * Several display names changed in the documented certificate store type definitions. There are no changes to the internal type or parameter names, so no migration is necessary for currently configured stores. - * Display name for IISU changed to "IIS Bound Certificate". - * Display name for WinCert changed to "Windows Certificate". - * Display names for several Store and Entry parameters changed to be more descriptive and UI friendly. + * Display name for IISU changed to "IIS Bound Certificate". + * Display name for WinCert changed to "Windows Certificate". + * Display names for several Store and Entry parameters changed to be more descriptive and UI friendly. * Significant ReadMe cleanup 2.1.0 + * Fixed issue that was occurring during renewal when there were bindings outside of http and https like net.tcp * Added PAM registration/initialization documentation in README.md -* Resolved Null HostName error +* Resolved Null HostName error * Added WinCert Cert Store Type * Added custom property parser to not show any passwords * Removed any password references in trace logs and output settings in JSON format 2.0.0 + * Add support for re-enrollment jobs (On Device Key Generation) with the ability to specify a cryptographic provider. Specification of cryptographic provider allows HSM (Hardware Security Module) use. * Local PAM Support added (requires Universal Orchestrator Framework version 10.1) * Certificate store type changed from IISBin to IISU. See README for migration notes. - 1.1.3 + * Made WinRM port a store parameter * Made WinRM protocol a store parameter * IISWBin 1.1.3 upgrade script.sql added to upgrade from 1.1.2 1.1.0 + * Migrate to Universal Orchestrator (KF9 / .NET Core) * Perform Renewals using RenewalThumbprint 1.0.3 + * Add support for the SNI Flags when creating new bindings. Supported flags include: - * 0 No SNI + * 0 No SNI * 1 SNI Enabled * 2 Non SNI binding which uses Central Certificate Store * 3 SNI binding which uses Central Certificate Store * Last release to support Windows Orchestrator (KF8) 1.0.2 + * Remove dependence on Windows.Web.Administration on the orchestrator server. The agent will now use the local version on the managed server via remote PowerShell * add support for the IncludePortInSPN flag * add support to use credentials from Keyfactor for Add/Remove/Inventory jobs. diff --git a/IISU/ImplementedStoreTypes/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs index cbb22b39..c540188c 100644 --- a/IISU/ImplementedStoreTypes/Win/Inventory.cs +++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs @@ -110,7 +110,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { _logger.LogTrace(LogHandler.FlattenException(ex)); - var failureMessage = $"Inventory job failed for Site '{jobConfiguration.CertificateStoreDetails.StorePath}' on server '{jobConfiguration.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"Inventory job failed for Site '{jobConfiguration.CertificateStoreDetails.StorePath}' on server '{jobConfiguration.CertificateStoreDetails.ClientMachine}' with error: '{ex.Message}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index 440b4f15..a3d5a030 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -126,7 +126,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) { _logger.LogTrace(LogHandler.FlattenException(ex)); - var failureMessage = $"Management job {_operationType} failed on Store '{_storePath}' on server '{_clientMachineName}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"Management job {_operationType} failed on Store '{_storePath}' on server '{_clientMachineName}' with error: '{ex.Message}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/ImplementedStoreTypes/WinAdfs/Management.cs b/IISU/ImplementedStoreTypes/WinAdfs/Management.cs index 77c3607b..a1b33d2e 100644 --- a/IISU/ImplementedStoreTypes/WinAdfs/Management.cs +++ b/IISU/ImplementedStoreTypes/WinAdfs/Management.cs @@ -147,7 +147,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) { _logger.LogTrace(LogHandler.FlattenException(ex)); - var failureMessage = $"Management job {_operationType} failed on Store '{_storePath}' on server '{_clientMachineName}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"Management job {_operationType} failed on Store '{_storePath}' on server '{_clientMachineName}' with error: '{ex.Message}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs index bbf637e4..1eca1384 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs @@ -110,7 +110,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { _logger.LogTrace(LogHandler.FlattenException(ex)); - var failureMessage = $"SQL Inventory job failed for Site '{jobConfiguration.CertificateStoreDetails.StorePath}' on server '{jobConfiguration.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"SQL Inventory job failed for Site '{jobConfiguration.CertificateStoreDetails.StorePath}' on server '{jobConfiguration.CertificateStoreDetails.ClientMachine}' with error: '{ex.Message}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index c7cf289c..fc0c1681 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -207,7 +207,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) { _logger.LogTrace(LogHandler.FlattenException(ex)); - var failureMessage = $"Management job {config.OperationType} failed for Site '{config.CertificateStoreDetails.StorePath}' on server '{config.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"Management job {config.OperationType} failed for Site '{config.CertificateStoreDetails.StorePath}' on server '{config.CertificateStoreDetails.ClientMachine}' with error: '{ex.Message}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 9b7ad03d..f65136a5 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -564,94 +564,6 @@ private void SetExecutionPolicyUnrestricted() PS.Commands.Clear(); } } - private void InitializeLocalSessionOLD2() - { - _logger.LogTrace("Creating out-of-process Powershell Runspace."); - PowerShellProcessInstance psInstance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); - Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), psInstance); - rs.Open(); - PS.Runspace = rs; - - // Set execution policy - ignore informational messages - _logger.LogTrace("Setting Execution Policy to Unrestricted"); - SetExecutionPolicyUnrestricted(); - - // Load all scripts - _logger.LogTrace("Loading PowerShell scripts"); - var scriptFiles = GetScriptFiles(scriptFileLocation); - _logger.LogInformation($"Found {scriptFiles.Count} script file(s) to load"); - - foreach (var scriptFile in scriptFiles) - { - var fileName = Path.GetFileName(scriptFile); - - if (this.isADFSStore && fileName.ToLower().Contains("adfs")) - { - // Import ADFS module (CRITICAL!) - _logger.LogTrace("Importing ADFS module"); - try - { - PS.AddCommand("Import-Module").AddParameter("Name", "ADFS"); - var moduleResult = PS.Invoke(); - - if (PS.HadErrors) - { - _logger.LogWarning("ADFS module import had errors (may not be available on this machine)"); - foreach (var error in PS.Streams.Error) - { - _logger.LogWarning($" {error}"); - } - PS.Streams.Error.Clear(); - } - else - { - _logger.LogInformation("ADFS module imported successfully"); - } - - PS.Commands.Clear(); - } - catch (Exception ex) - { - _logger.LogWarning($"Could not import ADFS module: {ex.Message}"); - _logger.LogWarning("ADFS cmdlets may not be available"); - } - - _logger.LogTrace($"Skipping non-ADFS script: {fileName} for ADFS store type"); - continue; - } - - _logger.LogTrace($"Loading script: {fileName}"); - - PS.AddScript($". '{scriptFile}'"); - PS.Invoke(); - CheckErrors(); // Check errors for actual scripts - PS.Commands.Clear(); - } - - _logger.LogInformation("Local PowerShell session initialized successfully"); - } - private void InitializeLocalSessionOLD() - { - _logger.LogTrace("Creating out-of-process Powershell Runspace."); - PowerShellProcessInstance psInstance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); - Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), psInstance); - rs.Open(); - PS.Runspace = rs; - - _logger.LogTrace("Setting Execution Policy to Unrestricted"); - PS.AddScript("Set-ExecutionPolicy Unrestricted -Scope Process -Force"); - PS.Invoke(); // Ensure the script is invoked and loaded - CheckErrors(); - - PS.Commands.Clear(); // Clear commands after loading functions - - _logger.LogTrace("Setting script file into memory"); - PS.AddScript(". '" + scriptFileLocation + "'"); - PS.Invoke(); // Ensure the script is invoked and loaded - CheckErrors(); - - PS.Commands.Clear(); // Clear commands after loading functions - } public void Terminate() { @@ -1144,53 +1056,7 @@ public static string FindScriptsDirectory(string rootDirectory, string directory return null; } - private List GetScriptFiles(string scriptFileLocation) - { - /* - * Gets all .ps1 files from the scripts directory - * - * scriptFileLocation can be: - * - A file path: C:\MyApp\Scripts\WinCertScripts.ps1 - * - A directory path: C:\MyApp\Scripts - * - * Returns: List of full file paths to all .ps1 files - */ - - // Determine the scripts directory - string scriptsDirectory; - - if (File.Exists(scriptFileLocation)) - { - // It's a file path - get the directory - scriptsDirectory = Path.GetDirectoryName(scriptFileLocation); - _logger.LogTrace($"Script file provided: {scriptFileLocation}"); - _logger.LogTrace($"Using directory: {scriptsDirectory}"); - } - else if (Directory.Exists(scriptFileLocation)) - { - // It's already a directory - scriptsDirectory = scriptFileLocation; - _logger.LogTrace($"Script directory provided: {scriptFileLocation}"); - } - else - { - throw new DirectoryNotFoundException($"Scripts location not found: {scriptFileLocation}"); - } - - // Get all .ps1 files, excluding .example files - var scriptFiles = Directory.GetFiles(scriptsDirectory, "*.ps1") - .Where(f => !f.EndsWith(".example", StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (scriptFiles.Count == 0) - { - throw new FileNotFoundException($"No .ps1 files found in: {scriptsDirectory}"); - } - - _logger.LogTrace($"Found {scriptFiles.Count} script file(s): {string.Join(", ", scriptFiles.Select(Path.GetFileName))}"); - - return scriptFiles; - } public static string LoadScript(string scriptFileName) { _logger.LogTrace($"Attempting to load script {scriptFileName}"); From 9418667fede714876fcd70e7c9ed471dd49d0630 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 15 May 2026 18:12:33 -0500 Subject: [PATCH 32/53] Updated language for WinSQL binding concerns --- docsource/winsql.md | 47 +++++++++++++++++++++++++++++++++++++++ integration-manifest.json | 29 +++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/docsource/winsql.md b/docsource/winsql.md index 19af1afc..51efb185 100644 --- a/docsource/winsql.md +++ b/docsource/winsql.md @@ -8,3 +8,50 @@ The WinSql Certificate Store Type, referred to by its short name 'WinSql,' is de - **Limitations:** Users should be aware that for this store type to function correctly, certain permissions are necessary. While some advanced users successfully use non-administrator accounts with specific permissions, it is officially supported only with Local Administrator permissions. Complexities with interactions between Group Policy, WinRM, User Account Control, and other environmental factors may impede operations if not properly configured. +### Verifying a Certificate Binding + +After the orchestrator binds a certificate to a SQL Server instance, **SQL Server Configuration Manager (SSCM) may show an empty value in the Certificate dropdown** under SQL Server Network Configuration → Protocols → Properties → Certificate tab. This is a known display limitation of SSCM and does not indicate a problem with the binding. SSCM applies its own certificate eligibility filter when populating the dropdown and may exclude certificates that SQL Server itself loads and uses successfully, particularly certificates bound programmatically rather than through the SSCM UI. + +Use the following two-step process to confirm a binding is correct independently of SSCM. + +#### Step 1 — Confirm the thumbprint is written to the registry + +Run the following on the SQL Server machine, replacing `MSSQLSERVER` with your instance name if using a named instance: + +```powershell +$instance = "MSSQLSERVER" +$full = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance +(Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$full\MSSQLServer\SuperSocketNetLib" -Name "Certificate").ToUpper() +``` + +This should return the thumbprint of the bound certificate. If the value is empty, the binding was not written to the registry. + +#### Step 2 — Confirm SQL Server loaded the certificate + +After the SQL Server service restarts, it writes a confirmation to the SQL Server error log. Run the following to check: + +```powershell +$logPath = (Resolve-Path "C:\Program Files\Microsoft SQL Server\MSSQL*\MSSQL\Log\ERRORLOG").Path +Select-String -Path $logPath -Pattern "certificate" -CaseSensitive:$false | ForEach-Object { $_.Line } +``` + +A successful binding produces a line similar to the following: + +``` +The certificate [Cert Hash(sha1) "D54E6CFFD7DF55FF9610355025BD603D7C25A2D4"] was successfully loaded for encryption. +``` + +The thumbprint in this message should match the value returned in Step 1. If the log instead shows `was not found or was not loaded`, the SQL Server service account does not have read access to the certificate's private key — contact your administrator to review private key permissions. + +#### Note on `encrypt_option` + +Binding a certificate does not automatically encrypt all client connections. The certificate is loaded and ready for use, but SQL Server will only negotiate TLS for a given connection when either the client requests it (`Encrypt=True` in the connection string) or the server is configured to force encryption. To verify that TLS is active for a specific connection, execute the following after connecting to the instance: + +```sql +SELECT session_id, encrypt_option, net_transport +FROM sys.dm_exec_connections +WHERE session_id = @@SPID +``` + +`encrypt_option = TRUE` confirms TLS is in use for that connection. Whether to enforce encryption server-wide (Force Encryption setting in SSCM) is a separate operational decision outside the scope of the orchestrator. + diff --git a/integration-manifest.json b/integration-manifest.json index 82dd0a91..998b28ed 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -99,6 +99,15 @@ "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -198,6 +207,15 @@ "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -330,7 +348,7 @@ "Add": true, "Create": false, "Discovery": false, - "Enrollment": false, + "Enrollment": true, "Remove": true }, "Properties": [ @@ -396,6 +414,15 @@ "DefaultValue": "false", "Required": true, "Description": "Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation." + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ From 7f1cac9f081d5bbeb79e64bbcb3d4d273b7cbbd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 May 2026 23:13:14 +0000 Subject: [PATCH 33/53] docs: auto-generate README and documentation [skip ci] --- README.md | 81 ++++++++++++++++++- ...SU-custom-field-JEAEndpointName-dialog.svg | 49 +++++++++++ ...EndpointName-validation-options-dialog.svg | 39 +++++++++ .../IISU-custom-fields-store-type-dialog.svg | 16 +++- ...rt-custom-field-JEAEndpointName-dialog.svg | 49 +++++++++++ ...EndpointName-validation-options-dialog.svg | 39 +++++++++ ...inCert-custom-fields-store-type-dialog.svg | 16 +++- .../images/WinSql-basic-store-type-dialog.svg | 3 +- ...ql-custom-field-JEAEndpointName-dialog.svg | 49 +++++++++++ ...EndpointName-validation-options-dialog.svg | 39 +++++++++ ...WinSql-custom-fields-store-type-dialog.svg | 16 +++- .../bash/curl_create_store_types.sh | 29 ++++++- .../restmethod_create_store_types.ps1 | 29 ++++++- 13 files changed, 437 insertions(+), 17 deletions(-) create mode 100644 docsource/images/IISU-custom-field-JEAEndpointName-dialog.svg create mode 100644 docsource/images/IISU-custom-field-JEAEndpointName-validation-options-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-JEAEndpointName-dialog.svg create mode 100644 docsource/images/WinCert-custom-field-JEAEndpointName-validation-options-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-JEAEndpointName-dialog.svg create mode 100644 docsource/images/WinSql-custom-field-JEAEndpointName-validation-options-dialog.svg diff --git a/README.md b/README.md index b9f58d0a..7f07af8f 100644 --- a/README.md +++ b/README.md @@ -673,6 +673,7 @@ the Keyfactor Command Portal | ServerUsername | Server Username | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | Secret | | 🔲 Unchecked | | ServerPassword | Server Password | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | Secret | | 🔲 Unchecked | | ServerUseSsl | Use SSL | Determine whether the server uses SSL or not (This field is automatically created) | Bool | true | ✅ Checked | + | JEAEndpointName | JEA End Point Name | Name of the JEA endpoint to use for the session (This field is automatically created) | String | | 🔲 Unchecked | The Custom Fields tab should look like this: @@ -722,6 +723,13 @@ the Keyfactor Command Portal ![WinCert Custom Field - ServerUseSsl](docsource/images/WinCert-custom-field-ServerUseSsl-validation-options-dialog.svg) + ###### JEA End Point Name + Name of the JEA endpoint to use for the session (This field is automatically created) + + ![WinCert Custom Field - JEAEndpointName](docsource/images/WinCert-custom-field-JEAEndpointName-dialog.svg) + ![WinCert Custom Field - JEAEndpointName](docsource/images/WinCert-custom-field-JEAEndpointName-validation-options-dialog.svg) + + ##### Entry Parameters Tab | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | @@ -941,6 +949,7 @@ the Keyfactor Command Portal | ServerUsername | Server Username | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | Secret | | 🔲 Unchecked | | ServerPassword | Server Password | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | Secret | | 🔲 Unchecked | | ServerUseSsl | Use SSL | Determine whether the server uses SSL or not (This field is automatically created) | Bool | true | ✅ Checked | + | JEAEndpointName | JEA End Point Name | Name of the JEA endpoint to use for the session (This field is automatically created) | String | | 🔲 Unchecked | The Custom Fields tab should look like this: @@ -990,6 +999,13 @@ the Keyfactor Command Portal ![IISU Custom Field - ServerUseSsl](docsource/images/IISU-custom-field-ServerUseSsl-validation-options-dialog.svg) + ###### JEA End Point Name + Name of the JEA endpoint to use for the session (This field is automatically created) + + ![IISU Custom Field - JEAEndpointName](docsource/images/IISU-custom-field-JEAEndpointName-dialog.svg) + ![IISU Custom Field - JEAEndpointName](docsource/images/IISU-custom-field-JEAEndpointName-validation-options-dialog.svg) + + ##### Entry Parameters Tab | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | @@ -1069,6 +1085,53 @@ The WinSql Certificate Store Type, referred to by its short name 'WinSql,' is de - **Limitations:** Users should be aware that for this store type to function correctly, certain permissions are necessary. While some advanced users successfully use non-administrator accounts with specific permissions, it is officially supported only with Local Administrator permissions. Complexities with interactions between Group Policy, WinRM, User Account Control, and other environmental factors may impede operations if not properly configured. +#### Verifying a Certificate Binding + +After the orchestrator binds a certificate to a SQL Server instance, **SQL Server Configuration Manager (SSCM) may show an empty value in the Certificate dropdown** under SQL Server Network Configuration → Protocols → Properties → Certificate tab. This is a known display limitation of SSCM and does not indicate a problem with the binding. SSCM applies its own certificate eligibility filter when populating the dropdown and may exclude certificates that SQL Server itself loads and uses successfully, particularly certificates bound programmatically rather than through the SSCM UI. + +Use the following two-step process to confirm a binding is correct independently of SSCM. + +##### Step 1 — Confirm the thumbprint is written to the registry + +Run the following on the SQL Server machine, replacing `MSSQLSERVER` with your instance name if using a named instance: + +```powershell +$instance = "MSSQLSERVER" +$full = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -Name $instance +(Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$full\MSSQLServer\SuperSocketNetLib" -Name "Certificate").ToUpper() +``` + +This should return the thumbprint of the bound certificate. If the value is empty, the binding was not written to the registry. + +##### Step 2 — Confirm SQL Server loaded the certificate + +After the SQL Server service restarts, it writes a confirmation to the SQL Server error log. Run the following to check: + +```powershell +$logPath = (Resolve-Path "C:\Program Files\Microsoft SQL Server\MSSQL*\MSSQL\Log\ERRORLOG").Path +Select-String -Path $logPath -Pattern "certificate" -CaseSensitive:$false | ForEach-Object { $_.Line } +``` + +A successful binding produces a line similar to the following: + +``` +The certificate [Cert Hash(sha1) "D54E6CFFD7DF55FF9610355025BD603D7C25A2D4"] was successfully loaded for encryption. +``` + +The thumbprint in this message should match the value returned in Step 1. If the log instead shows `was not found or was not loaded`, the SQL Server service account does not have read access to the certificate's private key — contact your administrator to review private key permissions. + +##### Note on `encrypt_option` + +Binding a certificate does not automatically encrypt all client connections. The certificate is loaded and ready for use, but SQL Server will only negotiate TLS for a given connection when either the client requests it (`Encrypt=True` in the connection string) or the server is configured to force encryption. To verify that TLS is active for a specific connection, execute the following after connecting to the instance: + +```sql +SELECT session_id, encrypt_option, net_transport +FROM sys.dm_exec_connections +WHERE session_id = @@SPID +``` + +`encrypt_option = TRUE` confirms TLS is in use for that connection. Whether to enforce encryption server-wide (Force Encryption setting in SSCM) is a separate operational decision outside the scope of the orchestrator. + #### Supported Operations | Operation | Is Supported | @@ -1076,7 +1139,7 @@ The WinSql Certificate Store Type, referred to by its short name 'WinSql,' is de | Add | ✅ Checked | | Remove | ✅ Checked | | Discovery | 🔲 Unchecked | -| Reenrollment | 🔲 Unchecked | +| Reenrollment | ✅ Checked | | Create | 🔲 Unchecked | #### Store Type Creation @@ -1120,7 +1183,7 @@ the Keyfactor Command Portal | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | ✅ Checked | Determines if store type may be included in an Orchestrator blueprint | @@ -1157,6 +1220,7 @@ the Keyfactor Command Portal | ServerPassword | Server Password | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | Secret | | 🔲 Unchecked | | ServerUseSsl | Use SSL | Determine whether the server uses SSL or not (This field is automatically created) | Bool | true | ✅ Checked | | RestartService | Restart SQL Service After Cert Installed | Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation. | Bool | false | ✅ Checked | + | JEAEndpointName | JEA End Point Name | Name of the JEA endpoint to use for the session (This field is automatically created) | String | | 🔲 Unchecked | The Custom Fields tab should look like this: @@ -1213,6 +1277,13 @@ the Keyfactor Command Portal ![WinSql Custom Field - RestartService](docsource/images/WinSql-custom-field-RestartService-validation-options-dialog.svg) + ###### JEA End Point Name + Name of the JEA endpoint to use for the session (This field is automatically created) + + ![WinSql Custom Field - JEAEndpointName](docsource/images/WinSql-custom-field-JEAEndpointName-dialog.svg) + ![WinSql Custom Field - JEAEndpointName](docsource/images/WinSql-custom-field-JEAEndpointName-validation-options-dialog.svg) + + ##### Entry Parameters Tab | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | @@ -1480,6 +1551,7 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat | ServerUsername | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | | ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | + | JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | @@ -1509,6 +1581,7 @@ The Windows Certificate Universal Orchestrator extension implements 4 Certificat | Properties.ServerUsername | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | | Properties.ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | Properties.ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | + | Properties.JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | 3. **Import the CSV file to create the certificate stores** @@ -1566,6 +1639,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | ServerUsername | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | | ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | + | JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | @@ -1595,6 +1669,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | Properties.ServerUsername | Username used to log into the target server for establishing the WinRM session. Example: 'administrator' or 'domain\username'. (This field is automatically created) | | Properties.ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | Properties.ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | + | Properties.JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | 3. **Import the CSV file to create the certificate stores** @@ -1653,6 +1728,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | | RestartService | Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation. | + | JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | @@ -1683,6 +1759,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | Properties.ServerPassword | Password corresponding to the Server Username used to log into the target server. When establishing a SSH session from a Linux environment, the password must include the full SSH Private key. (This field is automatically created) | | Properties.ServerUseSsl | Determine whether the server uses SSL or not (This field is automatically created) | | Properties.RestartService | Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation. | + | Properties.JEAEndpointName | Name of the JEA endpoint to use for the session (This field is automatically created) | 3. **Import the CSV file to create the certificate stores** diff --git a/docsource/images/IISU-custom-field-JEAEndpointName-dialog.svg b/docsource/images/IISU-custom-field-JEAEndpointName-dialog.svg new file mode 100644 index 00000000..dd41fa81 --- /dev/null +++ b/docsource/images/IISU-custom-field-JEAEndpointName-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + JEAEndpointName + Display Name + + JEA End Point Name + Type + + String + + Default Value + + + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-field-JEAEndpointName-validation-options-dialog.svg b/docsource/images/IISU-custom-field-JEAEndpointName-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/IISU-custom-field-JEAEndpointName-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/IISU-custom-fields-store-type-dialog.svg b/docsource/images/IISU-custom-fields-store-type-dialog.svg index b0073a5c..286d9526 100644 --- a/docsource/images/IISU-custom-fields-store-type-dialog.svg +++ b/docsource/images/IISU-custom-fields-store-type-dialog.svg @@ -1,5 +1,5 @@  - + - + Edit Certificate Store Type @@ -24,7 +24,7 @@ Entry Parameters - + @@ -33,7 +33,7 @@ EDIT DELETE - Total: 6 + Total: 7 Display Name @@ -95,4 +95,12 @@ Use SSL Bool true + + + + + + + JEA End Point Name + String \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-JEAEndpointName-dialog.svg b/docsource/images/WinCert-custom-field-JEAEndpointName-dialog.svg new file mode 100644 index 00000000..dd41fa81 --- /dev/null +++ b/docsource/images/WinCert-custom-field-JEAEndpointName-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + JEAEndpointName + Display Name + + JEA End Point Name + Type + + String + + Default Value + + + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-field-JEAEndpointName-validation-options-dialog.svg b/docsource/images/WinCert-custom-field-JEAEndpointName-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/WinCert-custom-field-JEAEndpointName-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinCert-custom-fields-store-type-dialog.svg b/docsource/images/WinCert-custom-fields-store-type-dialog.svg index b0073a5c..286d9526 100644 --- a/docsource/images/WinCert-custom-fields-store-type-dialog.svg +++ b/docsource/images/WinCert-custom-fields-store-type-dialog.svg @@ -1,5 +1,5 @@  - + - + Edit Certificate Store Type @@ -24,7 +24,7 @@ Entry Parameters - + @@ -33,7 +33,7 @@ EDIT DELETE - Total: 6 + Total: 7 Display Name @@ -95,4 +95,12 @@ Use SSL Bool true + + + + + + + JEA End Point Name + String \ No newline at end of file diff --git a/docsource/images/WinSql-basic-store-type-dialog.svg b/docsource/images/WinSql-basic-store-type-dialog.svg index 57755d51..6213662f 100644 --- a/docsource/images/WinSql-basic-store-type-dialog.svg +++ b/docsource/images/WinSql-basic-store-type-dialog.svg @@ -57,7 +57,8 @@ Create Discovery - + + ODKG diff --git a/docsource/images/WinSql-custom-field-JEAEndpointName-dialog.svg b/docsource/images/WinSql-custom-field-JEAEndpointName-dialog.svg new file mode 100644 index 00000000..dd41fa81 --- /dev/null +++ b/docsource/images/WinSql-custom-field-JEAEndpointName-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + JEAEndpointName + Display Name + + JEA End Point Name + Type + + String + + Default Value + + + Depends On + + + SPN With Port + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-field-JEAEndpointName-validation-options-dialog.svg b/docsource/images/WinSql-custom-field-JEAEndpointName-validation-options-dialog.svg new file mode 100644 index 00000000..22f8bbd6 --- /dev/null +++ b/docsource/images/WinSql-custom-field-JEAEndpointName-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/WinSql-custom-fields-store-type-dialog.svg b/docsource/images/WinSql-custom-fields-store-type-dialog.svg index 905ad22d..c806c285 100644 --- a/docsource/images/WinSql-custom-fields-store-type-dialog.svg +++ b/docsource/images/WinSql-custom-fields-store-type-dialog.svg @@ -1,5 +1,5 @@  - + - + Edit Certificate Store Type @@ -24,7 +24,7 @@ Entry Parameters - + @@ -33,7 +33,7 @@ EDIT DELETE - Total: 7 + Total: 8 Display Name @@ -104,4 +104,12 @@ Restart SQL Service After Cert Ins... Bool false + + + + + + + JEA End Point Name + String \ No newline at end of file diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh index e3ce44bf..db34988b 100755 --- a/scripts/store_types/bash/curl_create_store_types.sh +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -80,6 +80,15 @@ curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/Certificate "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -183,6 +192,15 @@ curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/Certificate "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -319,7 +337,7 @@ curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/Certificate "Add": true, "Create": false, "Discovery": false, - "Enrollment": false, + "Enrollment": true, "Remove": true }, "Properties": [ @@ -385,6 +403,15 @@ curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/Certificate "DefaultValue": "false", "Required": true, "Description": "Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation." + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 index fefc2320..2809ae4d 100644 --- a/scripts/store_types/powershell/restmethod_create_store_types.ps1 +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -80,6 +80,15 @@ $Body = @' "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -183,6 +192,15 @@ $Body = @' "DefaultValue": "true", "Required": true, "Description": "Determine whether the server uses SSL or not (This field is automatically created)" + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ @@ -319,7 +337,7 @@ $Body = @' "Add": true, "Create": false, "Discovery": false, - "Enrollment": false, + "Enrollment": true, "Remove": true }, "Properties": [ @@ -385,6 +403,15 @@ $Body = @' "DefaultValue": "false", "Required": true, "Description": "Boolean value (true or false) indicating whether to restart the SQL Server service after installing the certificate. Example: 'true' to enable service restart after installation." + }, + { + "Name": "JEAEndpointName", + "DisplayName": "JEA End Point Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "Name of the JEA endpoint to use for the session (This field is automatically created)" } ], "EntryParameters": [ From cd9a40190cf567f11bd8ec0d7f17ea514ab32dae Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 18 Jun 2026 08:24:21 -0700 Subject: [PATCH 34/53] Updated JEA Documentation Fixed SQL Restart issue --- CHANGELOG.md | 19 +- IISU/PSHelper.cs | 20 +- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 183 ++++++++++++++++-- .../Set-KeyfactorSQLCertificateBinding.ps1 | 53 +---- docsource/content.md | 116 ++++++----- 5 files changed, 268 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ac27af..7c402a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ * Adding JEA Support for local PowerShell execution. This will allow for more secure execution of the extension when running in a local PowerShell Runspace. To utilize this feature, you will need to create a JEA endpoint on the target server and specify the endpoint name as a new parameter in the specific Cert Store definition. Refer to the README for more details. * .NET6 assemblies are no longer supported. +3.0.2 + +* Fixed SQL service restart behavior: previously the extension could stop multiple SQL services and fail to start all of them. It now restarts only the SQL service associated with the certificate being renewed. + 3.0.1 -* Fixed an issues when renewing ECC Certificates +* Fixed an issues when renewing ECC Certificates 3.0.0 @@ -17,29 +21,29 @@ * Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags * Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags * Added SSL Flag validation to make sure the bit flag is correct. These are the valid bit flags for the version of Windows: - ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): + ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): - * 0 No SNI + * 0 No SNI * 1 Use SNI * 2 Use Centralized SSL certificate store. - ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): - * 0 No SNI + ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): + * 0 No SNI * 1 Use SNI * 4 Disable HTTP/2. ### Windows Server 2019 (IIS 10.0.17763) - * 0 No SNI + * 0 No SNI * 1 Use SNI * 4 Disable HTTP/2. * 8 Disable OCSP Stapling. ### Windows Server 2022+ (IIS 10.0.20348+) - * 0 No SNI + * 0 No SNI * 1 Use SNI * 4 Disable HTTP/2. * 8 Disable OCSP Stapling. @@ -165,6 +169,7 @@ * Fixed issue where https binding without cert was causing an error 2.2.0 + * Added Support for GMSA Account by using no value for ServerUsernanme and ServerPassword. KF Command version 10.2 or later is required to specify empty credentials. * Added local PowerShell support, triggered when specifying 'localhost' as the client machine while using the IISU or WinCert Orchestrator. This change was tested using KF Command 10.3 diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index f65136a5..be837487 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -333,9 +333,25 @@ private void InitializeRemoteSession() } else { - throw new Exception("Failed to create the remote PowerShell Session."); - } + // Attempt to extract error details from the PowerShell error stream + var errorDetails = new StringBuilder(); + + if (PS.HadErrors && PS.Streams.Error.Count > 0) + { + foreach (var error in PS.Streams.Error) + { + if (error == null) continue; + errorDetails.AppendLine(error.Exception?.Message ?? error.ToString()); + } + } + + var errorSummary = errorDetails.Length > 0 + ? $" Errors:{Environment.NewLine}{errorDetails.ToString().TrimEnd()}" + : " No errors were recorded in the PowerShell error stream."; + throw new Exception( + $"Failed to create the remote PowerShell session to '{machineName}'. {errorSummary}"); + } } private void InitializeLocalSession() diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc index c600a4d2..8f1b32b1 100644 --- a/IISU/PowerShell/Build/KeyfactorWinCert.pssc +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -1,12 +1,59 @@ # # KeyfactorWinCert.pssc # JEA Session Configuration file for Keyfactor Windows Certificate Store management +# ============================================================ +# OVERVIEW +# ============================================================ +# +# This file defines a JEA (Just Enough Administration) endpoint that controls: +# +# 1. WHO can connect -- defined in RoleDefinitions (and the Permission +# list set when registering with Register-PSSessionConfiguration) +# 2. WHAT they can do -- defined by the RoleCapabilities (.psrc files) +# referenced inside RoleDefinitions +# 3. WHAT IDENTITY executes -- defined by GroupManagedServiceAccount (gMSA) +# or RunAsVirtualAccount +# +# The recommended production pattern uses TWO separate gMSA accounts: +# +# LOW-PRIV gMSA (e.g. AD\KeyfactorSVC$) +# - The identity your Windows Service or orchestrator runs as. +# - Used only to AUTHENTICATE to WinRM on the remote machine. +# - Needs no special rights beyond WinRM access. +# - Listed in RoleDefinitions as the connecting identity. +# - Password is managed automatically by Active Directory. +# - Never needs to be entered or stored anywhere. +# +# HIGH-PRIV gMSA (e.g. AD\KeyfactorJEA$) +# - The identity that commands EXECUTE AS inside the JEA session. +# - Granted only the specific rights needed (e.g. IIS, cert store, SQL). +# - Set via GroupManagedServiceAccount -- never exposed over the network. +# - Password is managed automatically by Active Directory. +# - Cannot be used to open an interactive session directly. +# +# Visual flow: +# +# Windows Service WinRM / JEA Remote Resource +# (AD\KeyfactorSVC$) --> Endpoint (IIS, Cert Store) +# | 'keyfactor.wincert' ^ +# | | | +# | authenticates | RoleDefinitions | +# | via Kerberos | match KeyfactorSVC$ | +# | | | +# | | session runs AS | +# | | AD\KeyfactorJEA$ --------+ +# | | (has IIS/cert rights) +# +------------------------+ +# No password needed -- +# Kerberos handles it # # ============================================================ # PREREQUISITES (run once per target machine as Administrator) # ============================================================ # -# 1. Install the modules to the system module path so they are treated as trusted code: +# 1. Install the Keyfactor modules to the system module path. +# Modules placed here are treated as FULLY TRUSTED by PowerShell, +# which is required for them to work under ConstrainedLanguage mode. # # $base = 'C:\Program Files\WindowsPowerShell\Modules' # Copy-Item -Path '.\Keyfactor.WinCert.Common' -Destination "$base\Keyfactor.WinCert.Common" -Recurse -Force @@ -15,12 +62,28 @@ # # (Only install the modules needed for the store types you use on this endpoint.) # -# 2. (Optional) Create the transcript directory for audit logging. +# 2. Install the gMSA accounts on this machine (PRODUCTION only). +# This must be done on EACH machine that will host the JEA endpoint. +# The machine must be in the gMSA's PrincipalsAllowedToRetrieveManagedPassword group in AD. +# The gMSA accounts are only shown for example -- your AD administrator may have named them differently. +# +# # Install the HIGH-PRIV RunAs gMSA (executes the commands) +# Install-ADServiceAccount -Identity 'KeyfactorJEA$' +# Test-ADServiceAccount -Identity 'KeyfactorJEA$' # Must return True +# +# # Install the LOW-PRIV connecting gMSA (authenticates to WinRM) +# Install-ADServiceAccount -Identity 'KeyfactorSVC$' +# Test-ADServiceAccount -Identity 'KeyfactorSVC$' # Must return True +# +# If Test-ADServiceAccount returns False, the machine has not been added to +# PrincipalsAllowedToRetrieveManagedPassword in AD. Contact your AD administrator. +# +# 3. (Optional) Create the transcript directory for audit logging. # Only required if TranscriptDirectory is uncommented in the session configuration below. # # New-Item -ItemType Directory -Path 'C:\ProgramData\Keyfactor\JEA\Transcripts' -Force # -# 3. Register this session configuration (required once; re-run after any change): +# 4. Register this session configuration (required once; re-run after any change): # # Register-PSSessionConfiguration ` # -Name 'keyfactor.wincert' ` @@ -28,11 +91,13 @@ # -Force # Restart-Service WinRM # -# 4. Verify the endpoint is registered: +# The Name parameter is the endpoint name that the cert store will specify when connecting (e.g. New-PSSession -ConfigurationName 'keyfactor.wincert'). +# +# 5. Verify the endpoint is registered: # # Get-PSSessionConfiguration | Where-Object Name -eq 'keyfactor.wincert' # -# 5. To update or remove the endpoint: +# 6. To update or remove the endpoint: # # Unregister-PSSessionConfiguration -Name 'keyfactor.wincert' # Restart-Service WinRM @@ -70,15 +135,55 @@ # C:\Program Files\WindowsPowerShell\Modules\ run as FULLY TRUSTED regardless of this setting. LanguageMode = 'ConstrainedLanguage' + # RUN-AS ACCOUNT + # ------------------------------------------------------------------ + # This defines WHAT IDENTITY executes commands inside the session. + # The connecting user's own rights are NOT used -- all commands run + # as this account regardless of who connected. + # + # CHOOSE ONE of the following options: + # + # OPTION A -- Group Managed Service Account (PRODUCTION RECOMMENDED) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Use a gMSA that has been granted only the rights needed on this machine. + # AD manages the password automatically -- no password rotation or storage needed. + # The gMSA must be installed on this machine first (see PREREQUISITES, Step 2). + # + # IMPORTANT: Do NOT include the trailing '$' here. + # Windows automatically appends '$' when registering the configuration. + # If you include '$' yourself, Windows will register it as 'AD\KeyfactorJEA$$' + # and all connection attempts will fail. + # + # Correct: GroupManagedServiceAccount = 'AD\KeyfactorJEA' <-- no $ + # Incorrect: GroupManagedServiceAccount = 'AD\KeyfactorJEA$' <-- breaks registration + # + # Uncomment and set for PRODUCTION: + # + GroupManagedServiceAccount = 'AD\KeyfactorJEA' + # + # Replace 'AD' with your actual NetBIOS domain name (e.g. DOMAIN\KeyfactorJEA). + # + # + # OPTION B -- Virtual Account (DEVELOPMENT / TESTING ONLY) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Windows auto-creates a temporary local admin account for the duration + # of the session. No AD account or gMSA setup required. + # NOT recommended for production -- the virtual account has local admin + # rights and cannot be granted specific AD resource permissions. + # + # Comment out GroupManagedServiceAccount above and uncomment for DEV/TEST: + # + # RunAsVirtualAccount = $true + # --- Run-As Account --- # For development/testing: RunAsVirtualAccount creates a temporary local admin account. # For production: comment out RunAsVirtualAccount and use a Group Managed Service Account # that has been granted only the rights needed (read/write the certificate store). # # Production example: - # GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA$' + # GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA' # - RunAsVirtualAccount = $true + # RunAsVirtualAccount = $true # --- Transcript Logging (Optional) --- # Uncomment TranscriptDirectory to record a full transcript of every JEA session. @@ -90,19 +195,69 @@ # TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' # --- Role Definitions --- - # Map the connecting user/group to one or more Keyfactor role capabilities. - # Replace 'BUILTIN\Administrators' with the AD group or local group whose members - # are authorized to connect (e.g. 'DOMAIN\KeyfactorOrchestrators'). + # Maps the CONNECTING IDENTITY to one or more RoleCapabilities. + # This controls WHO is allowed in and WHAT commands they can run. + # + # Key points: + # - The connecting identity is authenticated by WinRM BEFORE this mapping is checked. + # - Multiple capabilities are merged -- the session exposes all their VisibleFunctions. + # - An identity not listed here will be denied access even if WinRM lets them connect. + # - Group membership is evaluated at connection time (e.g. BUILTIN\Administrators + # matches any local admin or domain admin on this machine). + # - gMSA accounts MUST include the trailing '$' in the account name. + # + # Available RoleCapabilities (add only modules installed on this machine): + # 'Keyfactor.WinCert.Common' -- core certificate store operations (always include) + # 'Keyfactor.WinCert.IIS' -- IIS binding management + # 'Keyfactor.WinCert.SQL' -- SQL certificate management # - # Add only the role capabilities whose modules are installed on this machine. - # Multiple capabilities are merged — the session exposes all their VisibleFunctions. + # gMSA CONNECTING IDENTITY PATTERN + # ------------------------------------------------------------------ + # When a Windows Service runs as a gMSA (e.g. AD\KeyfactorSVC$), it connects + # to WinRM using Kerberos automatically -- no credentials are passed in code. + # To allow this, add the LOW-PRIV gMSA to RoleDefinitions as shown below. # - # Examples: + # The low-priv gMSA (KeyfactorSVC$) authenticates the connection. + # The high-priv gMSA (KeyfactorJEA$, set via GroupManagedServiceAccount above) + # executes the commands. These are intentionally two different accounts. + # + # To add a gMSA as a connecting identity: + # 'DOMAIN\gMSAName$' = @{ RoleCapabilities = '...' } + # + # To add an AD security group (recommended over individual user accounts): + # 'DOMAIN\GroupName' = @{ RoleCapabilities = '...' } + # + # Example of Role Definitions: # WinCert + IIS: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } # WinCert + SQL: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.SQL' } # WinCert + IIS + SQL: @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } # WinCert only: @{ RoleCapabilities = 'Keyfactor.WinCert.Common' } RoleDefinitions = @{ - 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } + # LOW-PRIV SERVICE gMSA -- the connecting identity used by the Windows Service. + # This account authenticates via Kerberos; no password is ever stored or passed. + # Once connected, commands execute as AD\KeyfactorJEA$ (the RunAs gMSA above), + # not as KeyfactorSVC$ -- so KeyfactorSVC$ itself needs no elevated rights. + # Replace 'AD' with your NetBIOS domain name. The trailing '$' is required. + 'AD\KeyfactorSVC$' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } + + # AD SECURITY GROUP (recommended for human operator access). + # Using a group rather than individual accounts means you manage membership + # in AD without having to re-register the JEA endpoint. + # Uncomment and set to your group when ready: + # 'AD\KeyfactorOperators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } + + # LOCAL/DOMAIN ADMINISTRATORS -- full access for break-glass and troubleshooting. + # BUILTIN\Administrators matches any member of the local Administrators group, + # which includes Domain Admins on domain-joined machines. Be aware that domain + # admins will connect through this rule and execute as KeyfactorJEA$, which + # means their individual identity is not directly reflected in resource audit logs. + # Consider removing this entry and using a dedicated AD group in production + # to enforce individual accountability. + # 'BUILTIN\Administrators' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS', 'Keyfactor.WinCert.SQL' } + + # INDIVIDUAL USER ACCOUNTS -- useful during initial setup and testing. + # For production, prefer AD security groups over individual accounts to reduce + # the maintenance burden of updating this file when personnel change. + # 'AD\UserAccount' = @{ RoleCapabilities = 'Keyfactor.WinCert.Common', 'Keyfactor.WinCert.IIS' } } } diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 index d593668b..ffd98116 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1 @@ -452,56 +452,16 @@ try { # ============================================================ if ($RestartService) { Write-Information "Restarting SQL Server service..." - + try { - # Get current service status - $service = Get-Service -Name $serviceName -ErrorAction Stop - $originalStatus = $service.Status - - Write-Information "[VERBOSE] Current service status: $originalStatus" - - # Stop the service if running - if ($originalStatus -eq 'Running') { - Write-Information "Stopping SQL Server service: $serviceName" - Stop-Service -Name $serviceName -Force -ErrorAction Stop - - # Wait for service to stop (with timeout) - $stopTimeout = 60 - $elapsed = 0 - - while ((Get-Service -Name $serviceName).Status -ne 'Stopped' -and $elapsed -lt $stopTimeout) { - Start-Sleep -Seconds 2 - $elapsed += 2 - Write-Information "[VERBOSE] Waiting for service to stop... ($elapsed seconds)" - } - - if ((Get-Service -Name $serviceName).Status -ne 'Stopped') { - throw "Service did not stop within $stopTimeout seconds" - } - - Write-Information "SQL Server service stopped successfully" - } - - # Start the service - Write-Information "Starting SQL Server service: $serviceName" - Start-Service -Name $serviceName -ErrorAction Stop - - # Wait for service to start (with timeout) - $startTimeout = 90 - $elapsed = 0 - - while ((Get-Service -Name $serviceName).Status -ne 'Running' -and $elapsed -lt $startTimeout) { - Start-Sleep -Seconds 2 - $elapsed += 2 - Write-Information "[VERBOSE] Waiting for service to start... ($elapsed seconds)" - } - + Restart-Service -Name $serviceName -Force -ErrorAction Stop + $finalStatus = (Get-Service -Name $serviceName).Status - + if ($finalStatus -eq 'Running') { Write-Information "SQL Server service restarted successfully" } else { - throw "Service did not start within $startTimeout seconds. Current status: $finalStatus" + throw "Service restart completed but current status is: $finalStatus" } } catch { @@ -509,9 +469,6 @@ try { Write-Warning "Certificate binding completed but service restart failed." Write-Warning "Please restart SQL Server manually to apply the certificate binding." Write-Warning "You can restart using: Restart-Service -Name '$serviceName' -Force" - - # Don't throw here - the certificate binding succeeded - # Just warn the user to restart manually } } else { Write-Information "Service restart skipped. You must restart SQL Server for the certificate binding to take effect." diff --git a/docsource/content.md b/docsource/content.md index 10b581b2..223c5903 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -1,14 +1,16 @@ ## Overview + The Windows Certificate Orchestrator Extension is a multi-purpose integration that can remotely manage certificates on a Windows Server's Local Machine Store. This extension currently manages certificates for the current store types: + * WinADFS - Rotates the Service-Communications certificate on the primary and secondary ADFS nodes * WinCert - Certificates defined by path set for the Certificate Store -* WinIIS - IIS Bound certificates +* WinIIS - IIS Bound certificates * WinSQL - Certificates that are bound to the specified SQL Instances By default, most certificates are stored in the “Personal” (My) and “Web Hosting” (WebHosting) stores. For a complete list of local machine cert stores you can execute the PowerShell command: - Get-ChildItem Cert:\LocalMachine + Get-ChildItem Cert:\LocalMachine The returned list will contain the actual certificate store name to be used when entering store location. @@ -16,7 +18,7 @@ The ADFS extension performs both Inventory and Management Add jobs. The other e The Keyfactor Universal Orchestrator (UO) and WinCert Extension can be installed on either Windows or Linux operating systems. A UO service managing certificates on remote servers is considered to be acting as an Orchestrator, while a UO Service managing local certificates on the same server running the service is considered an Agent. When acting as an Orchestrator, connectivity from the orchestrator server hosting the WinCert extension to the orchestrated server hosting the certificate stores(s) being managed is achieved via either an SSH (for Linux orchestrated servers) or WinRM (for Windows orchestrated servers) connection. When acting as an agent (Windows only), WinRM may still be used, OR the certificate store can be configured to bypass a WinRM connection and instead directly access the orchestrator server's certificate stores. -![](images/orchestrator-agent.png) +![image](images/orchestrator-agent.png) Please refer to the READMEs for each supported store type for more information on proper configuration and setup for these different stores. The supported configurations of Universal Orchestrator hosts and managed orchestrated servers are detailed below: @@ -29,6 +31,7 @@ WinRM is used to remotely manage the certificate stores and IIS bindings on Wind **Note:** In version 2.0 of the IIS Orchestrator, the certificate store type has been renamed and additional parameters have been added. Prior to 2.0 the certificate store type was called “IISBin” and as of 2.0 it is called “IISU”. If you have existing certificate stores of type “IISBin”, you have three options: + 1. Leave them as is and continue to manage them with a pre 2.0 IIS Orchestrator Extension. Create the new IISU certificate store type and create any new IIS stores using the new type. 1. Delete existing IIS stores. Delete the IISBin store type. Create the new IISU store type. Recreate the IIS stores using the new IISU store type. 1. Convert existing IISBin certificate stores to IISU certificate stores. There is not currently a way to do this via the Keyfactor API, so direct updates to the underlying Keyfactor SQL database is required. A SQL script (IIS-Conversion.sql) is available in the repository to do this. Hosted customers, which do not have access to the underlying database, will need to work Keyfactor support to run the conversion. On-premises customers can run the script themselves, but are strongly encouraged to ensure that a SQL backup is taken prior running the script (and also be confident that they have a tested database restoration process.) @@ -47,7 +50,8 @@ In version 2.0 of the IIS Orchestrator, the certificate store type has been rena 2. SSH Authentication: When creating a Keyfactor certificate store for the WinCert orchestrator extension, the only protocol supported to communicate with Windows servers is ssh. When providing the user id and password, the connection is attempted by creating a temporary private key file using the contents in the Password textbox. Therefore, the password field must contain the full SSH Private key. 3. If you choose to run this extension in a containerized environment, the container image must include PowerShell version 7.5 or later, along with either OpenSSH clients (for SSH-based connections) or OpenSSL (if SSL/TLS operations are required). Additionally, the PWSMan PowerShell module must be installed to support management tasks and remote session functionality. These dependencies are required to ensure full compatibility when connecting from the container to remote Windows servers. Below is an example Docker file snippet: -``` +```text + dnf install https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell-7.5.2-1.rh.x86_64.rpm pwsh -Command 'Install-Module -Name PSWSMan' dnf install openssh-clients openssl @@ -75,9 +79,9 @@ Just Enough Administration (JEA) is a PowerShell security technology built into JEA operates through two types of configuration files: -- **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. +* **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. -- **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. +* **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `ConstrainedLanguage` PowerShell session backed by pre-installed, fully-trusted module code. The orchestrator can invoke Keyfactor certificate management functions, but nothing else. Every command executed in the session is recorded to a transcript file for audit purposes. @@ -87,10 +91,10 @@ When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `Co The default WinRM connection model requires the orchestrator service account to have local administrator rights on every managed server. While functional, this violates the principle of least privilege and creates a broad attack surface — if the service account credentials were ever compromised, an attacker would have administrative access to every managed server. JEA addresses this by: -- **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. -- **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. -- **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. -- **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. +* **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. +* **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. +* **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. +* **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. --- @@ -110,10 +114,10 @@ When the **JEA Endpoint Name** field is populated on a certificate store, the or Before configuring JEA on a target server, ensure the following: -- **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). -- **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. -- **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. -- **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. +* **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). +* **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. +* **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. +* **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. --- @@ -137,7 +141,7 @@ Install only the modules needed for the store types you manage on that server. F After deploying the Keyfactor Universal Orchestrator with the WinCert extension, navigate to the extension's output directory. You will find a `PowerShell` folder containing: -``` +```text PowerShell\ Keyfactor.WinCert.Common\ ← Module: common certificate operations Keyfactor.WinCert.IIS\ ← Module: IIS binding management @@ -226,13 +230,13 @@ Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working loca The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: -- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. +* **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. ```powershell RunAsVirtualAccount = $true ``` -- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. +* **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. ```powershell # Comment out RunAsVirtualAccount and uncomment this line: @@ -252,6 +256,8 @@ The JEA session executes the Keyfactor functions under a run-as account that is # Verify the gMSA can log on Test-ADServiceAccount -Identity 'KeyfactorJEA$' ``` + + These are only examples, your administrator may have Group Managed Service Accounts set up differently. Please consult with your administrator for more information on how to set up and use gMSAs in your environment. **Role Definitions (who is allowed to connect):** @@ -290,7 +296,7 @@ TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' ``` > **Recommendation:** Enable transcript logging during initial setup and testing. It makes it easy to confirm that the orchestrator is calling the correct functions with the correct parameters, and to diagnose any unexpected failures. Once you are confident the configuration is working correctly in production, you may choose to disable it to reduce disk usage — or keep it enabled to satisfy your organization's audit requirements. - +> > **Important:** If you enable `TranscriptDirectory`, you must also create the directory before registering the session configuration (Step 3). If the directory does not exist at registration time, `Register-PSSessionConfiguration` will fail. --- @@ -408,10 +414,11 @@ The orchestrator connected to the JEA session but the pre-flight check for `New- **"The term 'Get-KeyfactorCertificates' is not recognized..."** The function is not visible in the JEA session. Verify that: -- The module containing that function is installed on the target server (Step 2). -- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). -- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). -- The session configuration was re-registered and WinRM was restarted after any changes. + +* The module containing that function is installed on the target server (Step 2). +* The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). +* The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). +* The session configuration was re-registered and WinRM was restarted after any changes. **"Connecting user is not authorized to connect to this configuration"** @@ -420,9 +427,10 @@ The account used in the certificate store credentials is not a member of any gro **"Access is denied" or "WinRM cannot complete the operation"** This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: -- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). -- The WinRM firewall rule allows connections from the orchestrator server's IP. -- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. + +* WinRM is enabled on the target server (`Enable-PSRemoting -Force`). +* The WinRM firewall rule allows connections from the orchestrator server's IP. +* The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. **"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** @@ -447,18 +455,19 @@ Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | ### Important Notes and Limitations -- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. -- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. -- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. -- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. -- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. -- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. +* **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. +* **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. +* **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. +* **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. +* **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. +* **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. ### PowerShell Requirements + PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. @@ -475,27 +484,28 @@ In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatica ### Security and Permission Considerations From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. - + For customers wishing to use something other than the local administrator account, the following information may be helpful: - -* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). - -* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) - -* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. - -* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. - -* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - - Create and delete temporary files. - - Execute certreq commands. - - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. + +* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension).* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). * The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). + +* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account)* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) + +* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions.* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. * WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. + +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications.* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. * Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. + +* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to:* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: + - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. * Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. + - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. * Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. + - Create and delete temporary files. - Create and delete temporary files. * Create and delete temporary files. + - Execute certreq commands. - Execute certreq commands. * Execute certreq commands. + - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. * Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. + - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. * Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. ### Using Crypto Service Providers (CSP) -When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: @@ -508,13 +518,15 @@ When performing an Add job, if no CSP is specified, the machine's default CSP wi Each CSP only supports certain key types and algorithms. Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: + |CSP Name|Supports RSA?|Supports ECC?| |---|---|---| -|Microsoft RSA SChannel Cryptographic Provider |✅|❌| -|Microsoft Software Key Storage Provider |✅|✅| -|Microsoft Enhanced Cryptographic Provider |✅|❌| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| ## Client Machine Instructions + Prior to version 2.6, this extension would only run in the Windows environment. Version 2.6 and greater is capable of running on Linux, however, only the SSH protocol is supported. If running as an agent (accessing stores on the server where the Universal Orchestrator Services is installed ONLY), the Client Machine can be entered, OR you can bypass a WinRM connection and access the local file system directly by adding "|LocalMachine" to the end of your value for Client Machine, for example "1.1.1.1|LocalMachine". In this instance the value to the left of the pipe (|) is ignored. It is important to make sure the values for Client Machine and Store Path together are unique for each certificate store created, as Keyfactor Command requires the Store Type you select, along with Client Machine, and Store Path together must be unique. To ensure this, it is good practice to put the full DNS or IP Address to the left of the | character when setting up a certificate store that will be accessed without a WinRM connection. From 839753256a95789ad84bdab6dca20c17f3423d1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Jun 2026 15:25:08 +0000 Subject: [PATCH 35/53] docs: auto-generate README and documentation [skip ci] --- README.md | 129 +++++++++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 7f07af8f..5b15663e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -32,15 +32,16 @@ ## Overview The Windows Certificate Orchestrator Extension is a multi-purpose integration that can remotely manage certificates on a Windows Server's Local Machine Store. This extension currently manages certificates for the current store types: + * WinADFS - Rotates the Service-Communications certificate on the primary and secondary ADFS nodes * WinCert - Certificates defined by path set for the Certificate Store -* WinIIS - IIS Bound certificates +* WinIIS - IIS Bound certificates * WinSQL - Certificates that are bound to the specified SQL Instances By default, most certificates are stored in the “Personal” (My) and “Web Hosting” (WebHosting) stores. For a complete list of local machine cert stores you can execute the PowerShell command: - Get-ChildItem Cert:\LocalMachine + Get-ChildItem Cert:\LocalMachine The returned list will contain the actual certificate store name to be used when entering store location. @@ -48,7 +49,7 @@ The ADFS extension performs both Inventory and Management Add jobs. The other e The Keyfactor Universal Orchestrator (UO) and WinCert Extension can be installed on either Windows or Linux operating systems. A UO service managing certificates on remote servers is considered to be acting as an Orchestrator, while a UO Service managing local certificates on the same server running the service is considered an Agent. When acting as an Orchestrator, connectivity from the orchestrator server hosting the WinCert extension to the orchestrated server hosting the certificate stores(s) being managed is achieved via either an SSH (for Linux orchestrated servers) or WinRM (for Windows orchestrated servers) connection. When acting as an agent (Windows only), WinRM may still be used, OR the certificate store can be configured to bypass a WinRM connection and instead directly access the orchestrator server's certificate stores. -![](images/orchestrator-agent.png) +![image](images/orchestrator-agent.png) Please refer to the READMEs for each supported store type for more information on proper configuration and setup for these different stores. The supported configurations of Universal Orchestrator hosts and managed orchestrated servers are detailed below: @@ -61,6 +62,7 @@ WinRM is used to remotely manage the certificate stores and IIS bindings on Wind **Note:** In version 2.0 of the IIS Orchestrator, the certificate store type has been renamed and additional parameters have been added. Prior to 2.0 the certificate store type was called “IISBin” and as of 2.0 it is called “IISU”. If you have existing certificate stores of type “IISBin”, you have three options: + 1. Leave them as is and continue to manage them with a pre 2.0 IIS Orchestrator Extension. Create the new IISU certificate store type and create any new IIS stores using the new type. 1. Delete existing IIS stores. Delete the IISBin store type. Create the new IISU store type. Recreate the IIS stores using the new IISU store type. 1. Convert existing IISBin certificate stores to IISU certificate stores. There is not currently a way to do this via the Keyfactor API, so direct updates to the underlying Keyfactor SQL database is required. A SQL script (IIS-Conversion.sql) is available in the repository to do this. Hosted customers, which do not have access to the underlying database, will need to work Keyfactor support to run the conversion. On-premises customers can run the script themselves, but are strongly encouraged to ensure that a SQL backup is taken prior running the script (and also be confident that they have a tested database restoration process.) @@ -97,7 +99,8 @@ Before installing the Windows Certificate Universal Orchestrator extension, we r 2. SSH Authentication: When creating a Keyfactor certificate store for the WinCert orchestrator extension, the only protocol supported to communicate with Windows servers is ssh. When providing the user id and password, the connection is attempted by creating a temporary private key file using the contents in the Password textbox. Therefore, the password field must contain the full SSH Private key. 3. If you choose to run this extension in a containerized environment, the container image must include PowerShell version 7.5 or later, along with either OpenSSH clients (for SSH-based connections) or OpenSSL (if SSL/TLS operations are required). Additionally, the PWSMan PowerShell module must be installed to support management tasks and remote session functionality. These dependencies are required to ensure full compatibility when connecting from the container to remote Windows servers. Below is an example Docker file snippet: -``` +```text + dnf install https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell-7.5.2-1.rh.x86_64.rpm pwsh -Command 'Install-Module -Name PSWSMan' dnf install openssh-clients openssl @@ -125,9 +128,9 @@ Just Enough Administration (JEA) is a PowerShell security technology built into JEA operates through two types of configuration files: -- **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. +* **Session Configuration file (`.pssc`)** — Defines the overall session: the language mode, who is allowed to connect, which role capabilities to apply, whether to use a virtual run-as account or a Group Managed Service Account, and where to write audit transcripts. This file is registered with WinRM using `Register-PSSessionConfiguration` and becomes a named WinRM endpoint on the target server. -- **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. +* **Role Capability files (`.psrc`)** — Defines the functions, cmdlets, and external commands that are visible within the session to users assigned that role. Each Keyfactor module ships with its own `.psrc` file that whitelists only the functions required for certificate management. When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `ConstrainedLanguage` PowerShell session backed by pre-installed, fully-trusted module code. The orchestrator can invoke Keyfactor certificate management functions, but nothing else. Every command executed in the session is recorded to a transcript file for audit purposes. @@ -137,10 +140,10 @@ When the Keyfactor orchestrator connects to a JEA endpoint, it runs inside a `Co The default WinRM connection model requires the orchestrator service account to have local administrator rights on every managed server. While functional, this violates the principle of least privilege and creates a broad attack surface — if the service account credentials were ever compromised, an attacker would have administrative access to every managed server. JEA addresses this by: -- **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. -- **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. -- **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. -- **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. +* **Limiting command exposure** — The remote session only exposes the specific Keyfactor functions needed. An attacker with the service account credentials cannot run arbitrary commands or explore the target server. +* **Running as a privileged virtual or managed service account** — The connecting account itself does not need administrative rights. The JEA session can run the actual commands under a local virtual account or a Group Managed Service Account (gMSA) that has only the rights needed to manage certificates. +* **Full audit trail** — Every JEA session is automatically transcribed to a log file on the target server. You have a complete record of every function called, with what parameters, and at what time. +* **Simplified permission management** — Rather than managing complex local administrator group membership across dozens of servers, you create a single AD group of orchestrator service accounts that are permitted to connect to the JEA endpoint. --- @@ -160,10 +163,10 @@ When the **JEA Endpoint Name** field is populated on a certificate store, the or Before configuring JEA on a target server, ensure the following: -- **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). -- **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. -- **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. -- **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. +* **Windows PowerShell 5.1** is installed on the target server (included with Windows Server 2016 and later; available via Windows Management Framework 5.1 for Windows Server 2012 R2). +* **WinRM is enabled and configured** on the target server. Verify with: `Test-WSMan -ComputerName `. +* **The Keyfactor orchestrator deployment package** has been extracted. The `PowerShell` folder within the extension contains the module directories and JEA configuration files. +* **Local Administrator access** on the target server is required to perform the one-time JEA setup (registering the session configuration and installing modules). This is a setup-time requirement only — once configured, the orchestrator service account does not need administrator rights. --- @@ -187,7 +190,7 @@ Install only the modules needed for the store types you manage on that server. F After deploying the Keyfactor Universal Orchestrator with the WinCert extension, navigate to the extension's output directory. You will find a `PowerShell` folder containing: -``` +```text PowerShell\ Keyfactor.WinCert.Common\ ← Module: common certificate operations Keyfactor.WinCert.IIS\ ← Module: IIS binding management @@ -276,13 +279,13 @@ Copy the `KeyfactorWinCert.pssc` file from `PowerShell\Build\` to a working loca The JEA session executes the Keyfactor functions under a run-as account that is separate from the connecting account. There are two options: -- **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. +* **Virtual Account (default, recommended for testing):** A temporary local administrator account is automatically created for each JEA session and discarded when the session ends. This is the simplest option and requires no additional Active Directory configuration. ```powershell RunAsVirtualAccount = $true ``` -- **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. +* **Group Managed Service Account (recommended for production):** A gMSA runs the session under a domain account whose password is automatically managed by Active Directory. This is the preferred production option because it provides a stable, auditable identity without requiring manual password rotation. The gMSA must be created in Active Directory and granted the necessary permissions to manage certificates on the target server before use. ```powershell # Comment out RunAsVirtualAccount and uncomment this line: @@ -302,6 +305,8 @@ The JEA session executes the Keyfactor functions under a run-as account that is # Verify the gMSA can log on Test-ADServiceAccount -Identity 'KeyfactorJEA$' ``` + + These are only examples, your administrator may have Group Managed Service Accounts set up differently. Please consult with your administrator for more information on how to set up and use gMSAs in your environment. **Role Definitions (who is allowed to connect):** @@ -340,7 +345,7 @@ TranscriptDirectory = 'C:\ProgramData\Keyfactor\JEA\Transcripts' ``` > **Recommendation:** Enable transcript logging during initial setup and testing. It makes it easy to confirm that the orchestrator is calling the correct functions with the correct parameters, and to diagnose any unexpected failures. Once you are confident the configuration is working correctly in production, you may choose to disable it to reduce disk usage — or keep it enabled to satisfy your organization's audit requirements. - +> > **Important:** If you enable `TranscriptDirectory`, you must also create the directory before registering the session configuration (Step 3). If the directory does not exist at registration time, `Register-PSSessionConfiguration` will fail. --- @@ -458,10 +463,11 @@ The orchestrator connected to the JEA session but the pre-flight check for `New- **"The term 'Get-KeyfactorCertificates' is not recognized..."** The function is not visible in the JEA session. Verify that: -- The module containing that function is installed on the target server (Step 2). -- The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). -- The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). -- The session configuration was re-registered and WinRM was restarted after any changes. + +* The module containing that function is installed on the target server (Step 2). +* The corresponding role capability is listed in `RoleDefinitions` in the `.pssc` (Step 4). +* The module name in `RoleCapabilities` matches the module folder name exactly (case-sensitive on some systems). +* The session configuration was re-registered and WinRM was restarted after any changes. **"Connecting user is not authorized to connect to this configuration"** @@ -470,9 +476,10 @@ The account used in the certificate store credentials is not a member of any gro **"Access is denied" or "WinRM cannot complete the operation"** This typically indicates a WinRM connectivity issue rather than a JEA-specific problem. Verify that: -- WinRM is enabled on the target server (`Enable-PSRemoting -Force`). -- The WinRM firewall rule allows connections from the orchestrator server's IP. -- The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. + +* WinRM is enabled on the target server (`Enable-PSRemoting -Force`). +* The WinRM firewall rule allows connections from the orchestrator server's IP. +* The port (5985 for HTTP, 5986 for HTTPS) specified in the certificate store matches the WinRM listener configuration. **"Ambiguous configuration: the store target is set to the local machine but JEA endpoint is also configured"** @@ -497,18 +504,19 @@ Get-ChildItem 'C:\ProgramData\Keyfactor\JEA\Transcripts\' | ### Important Notes and Limitations -- **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. -- **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. -- **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. -- **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. -- **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. -- **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. +* **JEA is not supported over SSH.** JEA requires a WinRM connection. The SSH protocol does not support named session configurations and cannot be used to target a JEA endpoint. +* **JEA is not compatible with local machine (agent) mode.** If the Client Machine is set to `localhost`, `LocalMachine`, or uses `|LocalMachine`, the JEA endpoint name must be left empty. See the troubleshooting entry above. +* **One JEA endpoint can serve multiple store types.** A single `keyfactor.wincert` endpoint can expose Common, IIS, and SQL capabilities simultaneously. You do not need separate endpoints per store type — configure the role capabilities in the `.pssc` to include all modules installed on that server. +* **Module updates require re-copying files, not re-registration.** When the WinCert extension is upgraded, copy the updated module folders to the target server's `C:\Program Files\WindowsPowerShell\Modules\` directory. WinRM does not need to be restarted for module-only updates. +* **The JEA run-as account needs certificate store permissions.** Whether using a virtual account or a gMSA, the run-as account must have permission to read and write to the Windows certificate stores, access private keys, and (for IIS) manage IIS bindings. Virtual accounts are local administrators by default, so this is typically not a concern in development. For production gMSA accounts, explicitly grant the necessary permissions. +* **ADFS stores (WinADFS) do not support JEA.** The WinADFS store type requires specific ADFS module cmdlets that cannot be constrained within a JEA session. WinADFS stores must use a standard WinRM connection. Please consult with your company's system administrator for more information on configuring SSH or WinRM in your environment. ### PowerShell Requirements + PowerShell is extensively used to inventory and manage certificates across each Certificate Store Type. Windows Desktop and Server includes PowerShell 5.1 that is capable of running all or most PowerShell functions. If the Orchestrator is to run in a Linux environment using SSH as their communication protocol, PowerShell 6.1 or greater is required (7.4 or greater is recommended). In addition to PowerShell, IISU requires additional PowerShell modules to be installed and available. These modules include: WebAdministration and IISAdministration, versions 1.1. @@ -525,27 +533,28 @@ In standard (non-JEA) WinRM and local-machine modes, the orchestrator automatica ### Security and Permission Considerations From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. - + For customers wishing to use something other than the local administrator account, the following information may be helpful: - -* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). - -* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) - -* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. - -* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. - -* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - - Create and delete temporary files. - - Execute certreq commands. - - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. + +* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension).* The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). * The WinCert extensions (WinCert, IISU, WinSQL) create a WinRM (remote PowerShell) session to the target server in order to manipulate the Windows Certificate Stores, perform binding (in the case of the IISU extension), or to access the registry (in the case of the WinSQL extension). + +* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account)* When the WinRM session is created, the certificate store credentials are used if they have been specified, otherwise the WinRM session is created in the context of the Universal Orchestrator (UO) Service account (which potentially could be the network service account, a regular account, or a GMSA account) + +* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions.* WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. * WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. + +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications.* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. * Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. + +* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to:* The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: + - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. * Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. + - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. - Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. * Use the Import-Certificate, Get-WebSite, Get-WebBinding, and New-WebBinding PowerShell CmdLets. + - Create and delete temporary files. - Create and delete temporary files. * Create and delete temporary files. + - Execute certreq commands. - Execute certreq commands. * Execute certreq commands. + - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. * Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. + - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. * Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. ### Using Crypto Service Providers (CSP) -When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: @@ -558,11 +567,12 @@ When performing an Add job, if no CSP is specified, the machine's default CSP wi Each CSP only supports certain key types and algorithms. Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: + |CSP Name|Supports RSA?|Supports ECC?| |---|---|---| -|Microsoft RSA SChannel Cryptographic Provider |✅|❌| -|Microsoft Software Key Storage Provider |✅|✅| -|Microsoft Enhanced Cryptographic Provider |✅|❌| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| ## Certificate Store Types @@ -1483,9 +1493,9 @@ the Keyfactor Command Portal 1. **Download the latest Windows Certificate Universal Orchestrator extension from GitHub.** - Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/Windows Certificate Orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [Windows Certificate Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/iis-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `Windows Certificate Orchestrator` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `iis-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | | `11.6` _and_ newer | `net8.0` | | `net8.0` | @@ -1501,10 +1511,10 @@ the Keyfactor Command Portal 3. **Create a new directory for the Windows Certificate Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `Windows Certificate Orchestrator`. + Create a new directory called `iis-orchestrator`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `Windows Certificate Orchestrator` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `iis-orchestrator` directory.** 5. **Restart the Universal Orchestrator service.** @@ -1878,6 +1888,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov ## Client Machine Instructions + Prior to version 2.6, this extension would only run in the Windows environment. Version 2.6 and greater is capable of running on Linux, however, only the SSH protocol is supported. If running as an agent (accessing stores on the server where the Universal Orchestrator Services is installed ONLY), the Client Machine can be entered, OR you can bypass a WinRM connection and access the local file system directly by adding "|LocalMachine" to the end of your value for Client Machine, for example "1.1.1.1|LocalMachine". In this instance the value to the left of the pipe (|) is ignored. It is important to make sure the values for Client Machine and Store Path together are unique for each certificate store created, as Keyfactor Command requires the Store Type you select, along with Client Machine, and Store Path together must be unique. To ensure this, it is good practice to put the full DNS or IP Address to the left of the | character when setting up a certificate store that will be accessed without a WinRM connection. From 8720d8924c11e694fd721ddf39872e679695773f Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 25 Jun 2026 11:23:16 -0700 Subject: [PATCH 36/53] Added diagnostic cmdlet --- IISU/PowerShell/Build/KeyfactorWinCert.pssc | 12 +----------- .../Keyfactor.WinCert.Common.psm1 | 3 +++ .../Public/Get-KeyfactorDiagnostics.ps1 | 4 ++++ 3 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 diff --git a/IISU/PowerShell/Build/KeyfactorWinCert.pssc b/IISU/PowerShell/Build/KeyfactorWinCert.pssc index 8f1b32b1..1af65bf5 100644 --- a/IISU/PowerShell/Build/KeyfactorWinCert.pssc +++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc @@ -161,7 +161,7 @@ # GroupManagedServiceAccount = 'AD\KeyfactorJEA' # - # Replace 'AD' with your actual NetBIOS domain name (e.g. DOMAIN\KeyfactorJEA). + # Replace 'AD' with your actual NetBIOS domain name and the Group Managed Account that has been granted the necessary permissions (e.g. DOMAIN\{gMSA}). # # # OPTION B -- Virtual Account (DEVELOPMENT / TESTING ONLY) @@ -175,16 +175,6 @@ # # RunAsVirtualAccount = $true - # --- Run-As Account --- - # For development/testing: RunAsVirtualAccount creates a temporary local admin account. - # For production: comment out RunAsVirtualAccount and use a Group Managed Service Account - # that has been granted only the rights needed (read/write the certificate store). - # - # Production example: - # GroupManagedServiceAccount = 'DOMAIN\KeyfactorJEA' - # - # RunAsVirtualAccount = $true - # --- Transcript Logging (Optional) --- # Uncomment TranscriptDirectory to record a full transcript of every JEA session. # Useful during initial testing and for ongoing security audits. diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 index 35e7e6b3..7c5fb437 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 @@ -19,6 +19,8 @@ . "$PSScriptRoot\Public\Remove-KeyfactorCertificate.ps1" . "$PSScriptRoot\Public\New-KeyfactorODKGEnrollment.ps1" . "$PSScriptRoot\Public\Import-KeyfactorSignedCertificate.ps1" +. "$PSScriptRoot\Public\Get-KeyfactorDiagnostics.ps1" + # Export only public functions for non-JEA use. # In JEA sessions, VisibleFunctions in the .psrc is the actual access control mechanism. @@ -30,6 +32,7 @@ Export-ModuleMember -Function @( 'Remove-KeyfactorCertificate', 'New-KeyfactorODKGEnrollment', 'Import-KeyfactorSignedCertificate', + 'Get-KeyfactorDiagnostics', # Shared certificate inspection utilities — exported so other modules (e.g. IIS) can call them 'Get-CertificateCSP', 'Get-CertificateSAN' diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 new file mode 100644 index 00000000..f12bee56 --- /dev/null +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 @@ -0,0 +1,4 @@ +function Get-KeyfactorDiagnostics { + $x = (whoami /groups /fo csv | ConvertFrom-Csv | ForEach-Object { $n = if ([string]::IsNullOrWhiteSpace($_.'Group Name')) { $_.SID } else { $_.'Group Name' }; "$n ($($_.SID))" }) -join ', ' + Write-Information $x +} \ No newline at end of file From 970a9a8eaf3c3ae7d8e37a3f7b4d54b5056e1507 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 26 Jun 2026 11:38:40 -0700 Subject: [PATCH 37/53] Updated the JEA documentation. Added the get-keyfactordiagnostics cmdlet for troubleshooting. --- .../Public/Get-KeyfactorDiagnostics.ps1 | 178 +++++++++++++++++- .../Keyfactor.WinCert.Common.psrc | 1 + docsource/content.md | 57 +++++- 3 files changed, 233 insertions(+), 3 deletions(-) diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 index f12bee56..1061dbb9 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1 @@ -1,4 +1,178 @@ function Get-KeyfactorDiagnostics { - $x = (whoami /groups /fo csv | ConvertFrom-Csv | ForEach-Object { $n = if ([string]::IsNullOrWhiteSpace($_.'Group Name')) { $_.SID } else { $_.'Group Name' }; "$n ($($_.SID))" }) -join ', ' - Write-Information $x + [CmdletBinding()] + param() + + $InformationPreference = 'Continue' + $separator = '=' * 70 + $subSep = '-' * 70 + + $isConstrained = $ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage' + $isRemote = $null -ne $PSSenderInfo + + #region Header + Write-Information $separator + Write-Information " Keyfactor Diagnostics Report" + Write-Information " Generated : $(Get-Date)" + if ($isRemote) { Write-Information " Mode : Remote Session" } + if ($isConstrained) { Write-Information " *** Running in Constrained Language Mode (JEA) ***" } + if ($isConstrained) { Write-Information " *** Some sections will be skipped or limited ***" } + Write-Information $separator + #endregion + + #region Identity + Write-Information "" + Write-Information $subSep + Write-Information " Identity" + Write-Information $subSep + Write-Information " User : $(whoami)" + Write-Information " Display Name : $($env:USERNAME)" + Write-Information " Domain : $($env:USERDOMAIN)" + Write-Information " Computer : $($env:COMPUTERNAME)" + Write-Information " PS Version : $($PSVersionTable.PSVersion)" + Write-Information " PS Edition : $($PSVersionTable.PSEdition)" + Write-Information " OS : $($PSVersionTable.OS)" + + if (-not $isConstrained) { + $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + Write-Information " Run As Admin : $isAdmin" + } else { + Write-Information " Run As Admin : N/A (Constrained Language Mode)" + } + #endregion + + #region Session Info + Write-Information "" + Write-Information $subSep + Write-Information " Session Information" + Write-Information $subSep + Write-Information " Is Remote Session : $isRemote" + Write-Information " Runspace Id : $([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Id)" + Write-Information " Execution Policy : $(Get-ExecutionPolicy)" + Write-Information " Language Mode : $($ExecutionContext.SessionState.LanguageMode)" + + if ($PSSenderInfo) { + Write-Information " Connected User : $($PSSenderInfo.UserInfo.Identity.Name)" + Write-Information " Connection String : $($PSSenderInfo.ConnectionString)" + } + #endregion + + #region JEA + Write-Information "" + Write-Information $subSep + Write-Information " JEA (Just Enough Administration)" + Write-Information $subSep + $jeaConfigs = Get-PSSessionConfiguration -ErrorAction SilentlyContinue + if ($jeaConfigs) { + foreach ($config in $jeaConfigs) { + Write-Information " Endpoint : $($config.Name)" + Write-Information " Enabled : $($config.Enabled)" + Write-Information " Permission : $($config.Permission)" + Write-Information " PSVersion : $($config.PSVersion)" + Write-Information " Run As User : $($config.RunAsUser)" + Write-Information " Session Type : $($config.SessionType)" + Write-Information " Language Mode : $($config.LanguageMode)" + Write-Information " Startup Script : $($config.StartupScript)" + Write-Information " Role Definitions : $($config.RoleDefinitions)" + Write-Information "" + } + } else { + Write-Information " No PSSession configurations found or access denied." + } + #endregion + + #region WinRM + Write-Information "" + Write-Information $subSep + Write-Information " WinRM Service" + Write-Information $subSep + $winrm = Get-Service -Name WinRM -ErrorAction SilentlyContinue + Write-Information " WinRM Status : $($winrm.Status)" + Write-Information " WinRM StartType : $($winrm.StartType)" + + $winrmConfig = winrm get winrm/config 2>&1 + if ($winrmConfig -notmatch 'error') { + $maxShells = ($winrmConfig | Select-String 'MaxShellsPerUser') -replace '.*=\s*', '' + $maxMemory = ($winrmConfig | Select-String 'MaxMemoryPerShellMB') -replace '.*=\s*', '' + $maxTimeout = ($winrmConfig | Select-String 'MaxTimeoutms') -replace '.*=\s*', '' + Write-Information " Max Shells/User : $maxShells" + Write-Information " Max Memory (MB) : $maxMemory" + Write-Information " Max Timeout (ms) : $maxTimeout" + } else { + Write-Information " Could not retrieve WinRM config (may require elevation)." + } + + Write-Information "" + Write-Information " Listeners:" + $listeners = Get-ChildItem WSMan:\localhost\Listener -ErrorAction SilentlyContinue + foreach ($listener in $listeners) { + $props = Get-Item "WSMan:\localhost\Listener\$($listener.PSChildName)\*" -ErrorAction SilentlyContinue + Write-Information " [$($listener.PSChildName)]" + foreach ($prop in $props) { + Write-Information " $($prop.Name.PadRight(20)): $($prop.Value)" + } + } + #endregion + + #region Network + Write-Information "" + Write-Information $subSep + Write-Information " Network / Connectivity" + Write-Information $subSep + Write-Information " WinRM HTTP (5985) : $(Test-NetConnection -ComputerName localhost -Port 5985 -WarningAction SilentlyContinue | Select-Object -ExpandProperty TcpTestSucceeded)" + Write-Information " WinRM HTTPS (5986) : $(Test-NetConnection -ComputerName localhost -Port 5986 -WarningAction SilentlyContinue | Select-Object -ExpandProperty TcpTestSucceeded)" + #endregion + + #region Firewall + Write-Information "" + Write-Information $subSep + Write-Information " Firewall Rules (WinRM)" + Write-Information $subSep + $fwRules = Get-NetFirewallRule -DisplayGroup 'Windows Remote Management' -ErrorAction SilentlyContinue + if ($fwRules) { + foreach ($rule in $fwRules) { + Write-Information " $($rule.DisplayName.PadRight(45)) Enabled: $($rule.Enabled) Action: $($rule.Action) Direction: $($rule.Direction)" + } + } else { + Write-Information " No WinRM firewall rules found or access denied." + } + #endregion + + #region Group Memberships + Write-Information "" + Write-Information $subSep + Write-Information " Group Memberships" + Write-Information $subSep + $groups = whoami /groups /fo csv | ConvertFrom-Csv + foreach ($group in $groups) { + $name = if ([string]::IsNullOrWhiteSpace($group.'Group Name')) { $group.SID } else { $group.'Group Name' } + Write-Information " $($name.PadRight(50)) SID: $($group.SID)" + } + #endregion + + #region Privileges + Write-Information "" + Write-Information $subSep + Write-Information " User Privileges" + Write-Information $subSep + $privs = whoami /priv /fo csv | ConvertFrom-Csv + foreach ($priv in $privs) { + Write-Information " $($priv.'Privilege Name'.PadRight(45)) State: $($priv.State)" + } + #endregion + + #region Environment Variables + Write-Information "" + Write-Information $subSep + Write-Information " Relevant Environment Variables" + Write-Information $subSep + $relevantVars = @('PSModulePath', 'TEMP', 'TMP', 'PATH', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA', 'SystemRoot') + foreach ($var in $relevantVars) { + Write-Information " $($var.PadRight(20)): $([System.Environment]::GetEnvironmentVariable($var))" + } + #endregion + + Write-Information "" + Write-Information $separator + Write-Information " End of Diagnostics Report" + Write-Information $separator } \ No newline at end of file diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc index 68ddc77c..ab67eec4 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc @@ -33,6 +33,7 @@ # (e.g. Keyfactor.WinCert.IIS) call them cross-module, which requires them to be exported. VisibleFunctions = @( 'Get-KeyfactorCertificates', + 'Get-KeyfactorDiagnostics', 'Add-KeyfactorCertificate', 'Remove-KeyfactorCertificate', 'New-KeyfactorResult', diff --git a/docsource/content.md b/docsource/content.md index 223c5903..506ccc4d 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -345,6 +345,11 @@ $s = New-PSSession -ComputerName '' ` # List all commands available in the JEA session (should be limited to Keyfactor functions only) Invoke-Command -Session $s -ScriptBlock { Get-Command } +# Run the full diagnostic report — confirms identity, JEA configuration, WinRM, network, +# firewall, group memberships, privileges, and environment in a single command. +# -InformationAction Continue is required because the function writes to the Information stream. +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorDiagnostics } -InformationAction Continue + # Test a certificate inventory call (WinCert) Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } @@ -357,6 +362,8 @@ Remove-PSSession $s The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. +The `Get-KeyfactorDiagnostics` output is a multi-section health report covering the run-as account's identity and group memberships, the JEA configuration on the target, the WinRM service and listeners, network and firewall state, user privileges, and the environment variables (including `PSModulePath`) that PowerShell sees inside the session. This single command is the fastest way to confirm that everything from registration through to identity is configured correctly. See the **Troubleshooting** section below for details on interpreting each part of the report. + --- #### Step 7: Configure the Certificate Store in Keyfactor Command @@ -407,9 +414,57 @@ After removing the endpoint, any certificate stores in Keyfactor Command that re ### Troubleshooting +#### Quick Diagnostic: Using `Get-KeyfactorDiagnostics` + +For nearly every JEA setup and troubleshooting question — *Is the endpoint reachable? Is the run-as account what I expected? Does the run-as account have the right group memberships? Is WinRM healthy? Are the firewall rules in place? Where will PowerShell load modules from?* — the fastest first step is to run `Get-KeyfactorDiagnostics` inside the JEA session. This function is provided by the `Keyfactor.WinCert.Common` module and writes a multi-section health report covering identity, session/JEA configuration, WinRM, network connectivity, firewall, group memberships, privileges, and the relevant environment variables. + +**Why it matters:** In a JEA session, the account that authenticates the WinRM connection (the *connecting account*) and the account that actually executes the certificate management commands (the *run-as account*) are intentionally different. When a job fails with an "access denied" or similar permission error, the question is almost always *"what identity does the certificate store, IIS, or SQL service actually see at the moment the command runs?"* — and that identity is the run-as account, whose group memberships and privileges determine which ACLs it satisfies. + +**Run the diagnostic from any machine that can reach the target server:** + +```powershell +$cred = Get-Credential # Use the orchestrator service account credentials +$s = New-PSSession -ComputerName '' ` + -ConfigurationName 'keyfactor.wincert' ` + -Credential $cred + +# -InformationAction Continue is required so the diagnostic output is displayed +# (the function writes to the Information stream, not the Output stream). +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorDiagnostics } -InformationAction Continue + +Remove-PSSession $s +``` + +**What the report covers and how to use each section:** + +| Section | What it shows | What to look for | +|---|---|---| +| **Header** | Timestamp, whether the call is remote, and whether the session is in Constrained Language Mode (JEA). | The `*** Running in Constrained Language Mode (JEA) ***` banner confirms you are actually inside the JEA session and not accidentally running locally. | +| **Identity** | `whoami`, username, domain, computer, PowerShell version/edition, OS. | Confirms the run-as account is the one you configured. The `User` line is the actual run-as identity — compare it to the `GroupManagedServiceAccount` or `RunAsVirtualAccount` setting in the `.pssc`. A mismatch usually means the `.pssc` was not re-registered after a change. | +| **Session Information** | Runspace Id, execution policy, language mode, plus (when remote) the connecting user and connection string. | `Language Mode: ConstrainedLanguage` confirms the session is constrained. `Connected User` is who authenticated; the **Identity** section above is who actually runs commands. They should differ in a correctly configured JEA setup. | +| **JEA** | Every registered PSSession configuration on the target, with `Enabled`, `Permission`, `RunAsUser`, `SessionType`, `LanguageMode`, and `RoleDefinitions`. | Confirms `keyfactor.wincert` (or your chosen name) is registered, `Enabled: True`, set to `SessionType: RestrictedRemoteServer` + `LanguageMode: ConstrainedLanguage`, and lists the expected role bindings. If this section reports "access denied," the run-as account doesn't have rights to enumerate registered configurations — that's usually fine, but worth noting. | +| **WinRM Service** | WinRM service status and start type, throughput limits (`MaxShellsPerUser`, `MaxMemoryPerShellMB`, `MaxTimeoutms`), and configured listeners. | Service must be `Running`. The **Listeners** subsection shows which transports/ports are active — if the protocol you use (HTTP/5985 or HTTPS/5986) is missing here, the orchestrator cannot connect on that protocol no matter what the certificate store says. | +| **Network / Connectivity** | Local TCP tests for ports 5985 (HTTP) and 5986 (HTTPS). | Both should report `True` if the corresponding listener is running. `False` means WinRM isn't listening on that port, or the local firewall is blocking it. | +| **Firewall Rules (WinRM)** | All firewall rules in the `Windows Remote Management` display group, with `Enabled`, `Action`, and `Direction`. | The rule for the protocol you use, on the network profile your server is on (Domain / Private / Public), must be `Enabled: True` + `Action: Allow` + `Direction: Inbound`. If this section reports "access denied," the run-as account can't read firewall rules — re-run the diagnostic from an administrative session to see the rules. | +| **Group Memberships** | Every security group the run-as account belongs to, with SIDs. | This is **the key answer to most "access denied" errors**. The run-as account passes an ACL only if it (or a group listed here) was granted access. If the AD group you ACL'd is not in the list, the run-as account is not in that group. | +| **User Privileges** | Every Windows privilege held by the run-as account, with its `State` (Enabled / Disabled). | Useful when an operation that needs a specific privilege (for example `SeRestorePrivilege` or `SeBackupPrivilege`) fails — confirms the privilege is present **and** enabled in the token. A privilege listed as `Disabled` is not in effect even though it is granted. | +| **Environment Variables** | `PSModulePath`, `TEMP`, `TMP`, `PATH`, `PATHEXT`, `APPDATA`, `LOCALAPPDATA`, `SystemRoot` — as the run-as account sees them. | `PSModulePath` must include `C:\Program Files\WindowsPowerShell\Modules\` for the Keyfactor modules to load as trusted. `TEMP` must point to a writable directory for re-enrollment jobs that drop CSR/INF files. | + +**Important behavior notes:** + +* In a JEA session (`ConstrainedLanguage` mode), a small number of checks are skipped because they require unconstrained .NET access — for example, the `Run As Admin` line will read `N/A (Constrained Language Mode)`. The report header explicitly calls this out, and the rest of the report still runs. +* Sections that need elevation (WinRM config, firewall rules, registered PSSession configurations) gracefully report "access denied" when the run-as account doesn't have rights to read them, rather than failing the whole report. This is by design — the report keeps going so you still see the parts that did work. +* `Get-KeyfactorDiagnostics` is also exported by the module in standard (non-JEA) WinRM and local-machine modes, so the same command can be used against any orchestrator-managed server — useful for diagnosing the UAC token filtering and group policy issues described in the **Security and Permission Considerations** section. + +**During a real job run:** + +The function writes to the Information stream, which the orchestrator captures and forwards to its log file. If you need a permanent diagnostic snapshot tied to a specific failing job, you can call `Get-KeyfactorDiagnostics` from a quick interactive `Invoke-Command` immediately before re-running the job — the full report will appear in your terminal, and the orchestrator's log around the job-failure timestamp will show the run-as account's view of the world at the moment the job ran. + +--- + **"JEA endpoint is reachable but Keyfactor modules are not installed"** -The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). +The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). The **Environment Variables** section of `Get-KeyfactorDiagnostics` shows the exact `PSModulePath` the session is using — if `C:\Program Files\WindowsPowerShell\Modules\` is missing from it, that is the cause. **"The term 'Get-KeyfactorCertificates' is not recognized..."** From 3fe25ef154b697397b3e3f58e4ba4d83359e37a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jun 2026 18:39:23 +0000 Subject: [PATCH 38/53] docs: auto-generate README and documentation [skip ci] --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b15663e..bec9f958 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,11 @@ $s = New-PSSession -ComputerName '' ` # List all commands available in the JEA session (should be limited to Keyfactor functions only) Invoke-Command -Session $s -ScriptBlock { Get-Command } +# Run the full diagnostic report — confirms identity, JEA configuration, WinRM, network, +# firewall, group memberships, privileges, and environment in a single command. +# -InformationAction Continue is required because the function writes to the Information stream. +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorDiagnostics } -InformationAction Continue + # Test a certificate inventory call (WinCert) Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorCertificates -StoreName 'My' } @@ -406,6 +411,8 @@ Remove-PSSession $s The `Get-Command` output should show only a small set of Keyfactor functions plus the basic infrastructure cmdlets allowed by the session (e.g., `Write-Output`, `ConvertTo-Json`). If you see hundreds of commands, the session is not properly restricted and the session configuration should be reviewed. +The `Get-KeyfactorDiagnostics` output is a multi-section health report covering the run-as account's identity and group memberships, the JEA configuration on the target, the WinRM service and listeners, network and firewall state, user privileges, and the environment variables (including `PSModulePath`) that PowerShell sees inside the session. This single command is the fastest way to confirm that everything from registration through to identity is configured correctly. See the **Troubleshooting** section below for details on interpreting each part of the report. + --- #### Step 7: Configure the Certificate Store in Keyfactor Command @@ -456,9 +463,57 @@ After removing the endpoint, any certificate stores in Keyfactor Command that re ### Troubleshooting +#### Quick Diagnostic: Using `Get-KeyfactorDiagnostics` + +For nearly every JEA setup and troubleshooting question — *Is the endpoint reachable? Is the run-as account what I expected? Does the run-as account have the right group memberships? Is WinRM healthy? Are the firewall rules in place? Where will PowerShell load modules from?* — the fastest first step is to run `Get-KeyfactorDiagnostics` inside the JEA session. This function is provided by the `Keyfactor.WinCert.Common` module and writes a multi-section health report covering identity, session/JEA configuration, WinRM, network connectivity, firewall, group memberships, privileges, and the relevant environment variables. + +**Why it matters:** In a JEA session, the account that authenticates the WinRM connection (the *connecting account*) and the account that actually executes the certificate management commands (the *run-as account*) are intentionally different. When a job fails with an "access denied" or similar permission error, the question is almost always *"what identity does the certificate store, IIS, or SQL service actually see at the moment the command runs?"* — and that identity is the run-as account, whose group memberships and privileges determine which ACLs it satisfies. + +**Run the diagnostic from any machine that can reach the target server:** + +```powershell +$cred = Get-Credential # Use the orchestrator service account credentials +$s = New-PSSession -ComputerName '' ` + -ConfigurationName 'keyfactor.wincert' ` + -Credential $cred + +# -InformationAction Continue is required so the diagnostic output is displayed +# (the function writes to the Information stream, not the Output stream). +Invoke-Command -Session $s -ScriptBlock { Get-KeyfactorDiagnostics } -InformationAction Continue + +Remove-PSSession $s +``` + +**What the report covers and how to use each section:** + +| Section | What it shows | What to look for | +|---|---|---| +| **Header** | Timestamp, whether the call is remote, and whether the session is in Constrained Language Mode (JEA). | The `*** Running in Constrained Language Mode (JEA) ***` banner confirms you are actually inside the JEA session and not accidentally running locally. | +| **Identity** | `whoami`, username, domain, computer, PowerShell version/edition, OS. | Confirms the run-as account is the one you configured. The `User` line is the actual run-as identity — compare it to the `GroupManagedServiceAccount` or `RunAsVirtualAccount` setting in the `.pssc`. A mismatch usually means the `.pssc` was not re-registered after a change. | +| **Session Information** | Runspace Id, execution policy, language mode, plus (when remote) the connecting user and connection string. | `Language Mode: ConstrainedLanguage` confirms the session is constrained. `Connected User` is who authenticated; the **Identity** section above is who actually runs commands. They should differ in a correctly configured JEA setup. | +| **JEA** | Every registered PSSession configuration on the target, with `Enabled`, `Permission`, `RunAsUser`, `SessionType`, `LanguageMode`, and `RoleDefinitions`. | Confirms `keyfactor.wincert` (or your chosen name) is registered, `Enabled: True`, set to `SessionType: RestrictedRemoteServer` + `LanguageMode: ConstrainedLanguage`, and lists the expected role bindings. If this section reports "access denied," the run-as account doesn't have rights to enumerate registered configurations — that's usually fine, but worth noting. | +| **WinRM Service** | WinRM service status and start type, throughput limits (`MaxShellsPerUser`, `MaxMemoryPerShellMB`, `MaxTimeoutms`), and configured listeners. | Service must be `Running`. The **Listeners** subsection shows which transports/ports are active — if the protocol you use (HTTP/5985 or HTTPS/5986) is missing here, the orchestrator cannot connect on that protocol no matter what the certificate store says. | +| **Network / Connectivity** | Local TCP tests for ports 5985 (HTTP) and 5986 (HTTPS). | Both should report `True` if the corresponding listener is running. `False` means WinRM isn't listening on that port, or the local firewall is blocking it. | +| **Firewall Rules (WinRM)** | All firewall rules in the `Windows Remote Management` display group, with `Enabled`, `Action`, and `Direction`. | The rule for the protocol you use, on the network profile your server is on (Domain / Private / Public), must be `Enabled: True` + `Action: Allow` + `Direction: Inbound`. If this section reports "access denied," the run-as account can't read firewall rules — re-run the diagnostic from an administrative session to see the rules. | +| **Group Memberships** | Every security group the run-as account belongs to, with SIDs. | This is **the key answer to most "access denied" errors**. The run-as account passes an ACL only if it (or a group listed here) was granted access. If the AD group you ACL'd is not in the list, the run-as account is not in that group. | +| **User Privileges** | Every Windows privilege held by the run-as account, with its `State` (Enabled / Disabled). | Useful when an operation that needs a specific privilege (for example `SeRestorePrivilege` or `SeBackupPrivilege`) fails — confirms the privilege is present **and** enabled in the token. A privilege listed as `Disabled` is not in effect even though it is granted. | +| **Environment Variables** | `PSModulePath`, `TEMP`, `TMP`, `PATH`, `PATHEXT`, `APPDATA`, `LOCALAPPDATA`, `SystemRoot` — as the run-as account sees them. | `PSModulePath` must include `C:\Program Files\WindowsPowerShell\Modules\` for the Keyfactor modules to load as trusted. `TEMP` must point to a writable directory for re-enrollment jobs that drop CSR/INF files. | + +**Important behavior notes:** + +* In a JEA session (`ConstrainedLanguage` mode), a small number of checks are skipped because they require unconstrained .NET access — for example, the `Run As Admin` line will read `N/A (Constrained Language Mode)`. The report header explicitly calls this out, and the rest of the report still runs. +* Sections that need elevation (WinRM config, firewall rules, registered PSSession configurations) gracefully report "access denied" when the run-as account doesn't have rights to read them, rather than failing the whole report. This is by design — the report keeps going so you still see the parts that did work. +* `Get-KeyfactorDiagnostics` is also exported by the module in standard (non-JEA) WinRM and local-machine modes, so the same command can be used against any orchestrator-managed server — useful for diagnosing the UAC token filtering and group policy issues described in the **Security and Permission Considerations** section. + +**During a real job run:** + +The function writes to the Information stream, which the orchestrator captures and forwards to its log file. If you need a permanent diagnostic snapshot tied to a specific failing job, you can call `Get-KeyfactorDiagnostics` from a quick interactive `Invoke-Command` immediately before re-running the job — the full report will appear in your terminal, and the orchestrator's log around the job-failure timestamp will show the run-as account's view of the world at the moment the job ran. + +--- + **"JEA endpoint is reachable but Keyfactor modules are not installed"** -The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). +The orchestrator connected to the JEA session but the pre-flight check for `New-KeyfactorResult` failed. This means the Keyfactor modules are not installed in a location that PowerShell recognizes as trusted. Verify that the modules are installed under `C:\Program Files\WindowsPowerShell\Modules\` (not under the user profile or any other path) and that the module folder name exactly matches the module name (e.g., `Keyfactor.WinCert.Common`). The **Environment Variables** section of `Get-KeyfactorDiagnostics` shows the exact `PSModulePath` the session is using — if `C:\Program Files\WindowsPowerShell\Modules\` is missing from it, that is the cause. **"The term 'Get-KeyfactorCertificates' is not recognized..."** From e0da7d44c786e393545c04e40340162b311c1568 Mon Sep 17 00:00:00 2001 From: Bob Pokorny <55611381+rcpokorny@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:53:53 -0500 Subject: [PATCH 39/53] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Private/Get-KeyfactorSQLServiceUser.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 index 32649e79..ec2c5f1d 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Get-KeyfactorSQLServiceUser.ps1 @@ -10,7 +10,7 @@ function Get-KeyfactorSQLServiceUser { if ($serviceUser) { return $serviceUser } else { - Write-Error "SQL Server instance '$SQLInstanceName' not found or no service user associated." + Write-Error "SQL Server service '$SQLServiceName' not found or no service user associated." return $null } From bc07ae88e50da02ed1a8273573b93f2cbcbd259a Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 1 Jul 2026 15:18:44 -0700 Subject: [PATCH 40/53] Updated error reporting when bad CSP --- CHANGELOG.md | 5 +- IISU/ImplementedStoreTypes/Win/Management.cs | 35 +- .../WinIIS/Management.cs | 57 ++-- .../WinSQL/Management.cs | 58 +++- IISU/Models/ResultObject.cs | 119 ++++++- IISU/PSHelper.cs | 6 +- .../Public/Add-KeyfactorCertificate.ps1 | 300 ++++++++++-------- .../Public/New-KeyfactorResult.ps1 | 7 + IISU/WindowsCertStore.csproj | 1 - .../AddKeyfactorCertificateScriptTests.cs | 202 ++++++++++++ .../ResultObjectTests.cs | 208 ++++++++++++ 11 files changed, 817 insertions(+), 181 deletions(-) create mode 100644 WindowsCertStore.UnitTests/AddKeyfactorCertificateScriptTests.cs create mode 100644 WindowsCertStore.UnitTests/ResultObjectTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c402a1f..44c315e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * As of this version of the extension, SANs will be handled through the ODKG Enrollment page in Command and will no longer use the SAN Entry Parameter. This version, we are removing all support for the SAN Entry Parameter. If you are still using the SAN Entry Parameter, you will need to remove it from your store types and re-run inventory to remove it from your database. * Adding JEA Support for local PowerShell execution. This will allow for more secure execution of the extension when running in a local PowerShell Runspace. To utilize this feature, you will need to create a JEA endpoint on the target server and specify the endpoint name as a new parameter in the specific Cert Store definition. Refer to the README for more details. * .NET6 assemblies are no longer supported. +* Fixed a problem when passing a bad CSP, the job was reporting successful, but did not actually add/bind the certificate. 3.0.2 @@ -21,14 +22,14 @@ * Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags * Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags * Added SSL Flag validation to make sure the bit flag is correct. These are the valid bit flags for the version of Windows: - ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): + ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): * 0 No SNI * 1 Use SNI * 2 Use Centralized SSL certificate store. - ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): + ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): * 0 No SNI * 1 Use SNI diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index a3d5a030..63e92615 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -26,6 +26,7 @@ using Keyfactor.Logging; using System.Collections.ObjectModel; using System.Collections.Generic; +using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinCert { @@ -146,7 +147,7 @@ public JobResult AddCertificate(string certificateContents, string privateKeyPas { _psHelper.Initialize(); - _logger.LogTrace("Attempting to execute PS function (Add-KFCertificateToStore)"); + _logger.LogTrace("Attempting to execute PS function (Add-KeyfactorCertificate)"); // Mandatory parameters var parameters = new Dictionary @@ -162,17 +163,31 @@ public JobResult AddCertificate(string certificateContents, string privateKeyPas _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); _logger.LogTrace("Returned from executing PS function (Add-KeyfactorCertificate)"); - // This should return the thumbprint of the certificate - if (_results != null && _results.Count > 0) - { - var thumbprint = _results[0].ToString(); - _logger.LogTrace($"Added certificate to store {_storePath}, returned with the thumbprint {thumbprint}"); - } - else + ResultObject addResult = ResultObject.FromPSResults(_results); + _logger.LogTrace($"Add-KeyfactorCertificate returned Status={addResult.Status}, Code={addResult.Code}, Step={addResult.Step}, Thumbprint='{addResult.Thumbprint}'"); + + _psHelper.Terminate(); + + if (!addResult.IsSuccess) { - _logger.LogTrace("No results were returned. There could have been an error while adding the certificate. Look in the trace logs for PowerShell information."); + string detail = !string.IsNullOrEmpty(addResult.ErrorMessage) + ? addResult.ErrorMessage + : addResult.Message; + + string failureMessage = + $"Add certificate to store '{_storePath}' failed at step '{addResult.Step}' (code {addResult.Code}): {detail}"; + + _logger.LogWarning(failureMessage); + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = _jobHistoryID, + FailureMessage = failureMessage + }; } - _psHelper.Terminate(); + + _logger.LogTrace($"Added certificate to store {_storePath}, thumbprint {addResult.Thumbprint}"); } return new JobResult diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index 7f5ed986..99f6e8af 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -27,7 +27,6 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.Commands; using Newtonsoft.Json; - namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.IISU { public class Management : WinCertJobTypeBase, IManagementJobExtension @@ -122,9 +121,31 @@ public JobResult ProcessJob(ManagementJobConfiguration config) OrchestratorJobStatusJobResult psResult = OrchestratorJobStatusJobResult.Unknown; string failureMessage = ""; - - string newThumbprint = AddCertificate(certificateContents, privateKeyPassword, cryptoProvider); - _logger.LogTrace($"Completed adding the certificate to the store"); + + ResultObject addResult = AddCertificate(certificateContents, privateKeyPassword, cryptoProvider); + _logger.LogTrace($"Completed adding the certificate to the store. Status={addResult.Status}, Code={addResult.Code}, Step={addResult.Step}"); + + if (!addResult.IsSuccess) + { + string detail = !string.IsNullOrEmpty(addResult.ErrorMessage) + ? addResult.ErrorMessage + : addResult.Message; + + string addFailureMessage = + $"Add certificate to store '{_storePath}' failed at step '{addResult.Step}' (code {addResult.Code}): {detail}"; + + _logger.LogError(addFailureMessage); + + complete = new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = _jobHistoryID, + FailureMessage = addFailureMessage + }; + break; + } + + string newThumbprint = addResult.Thumbprint; _logger.LogTrace($"New thumbprint: {newThumbprint}"); // Bind Certificate to IIS Site @@ -201,8 +222,9 @@ public JobResult ProcessJob(ManagementJobConfiguration config) { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = _jobHistoryID, - FailureMessage = $"No thumbprint was returned. Unable to bind certificate to site: {bindingInfo.SiteName}." - }; } + FailureMessage = $"Add-KeyfactorCertificate reported Success but did not return a thumbprint. Unable to bind certificate to site: {bindingInfo.SiteName}." + }; + } } catch (Exception ex) { @@ -277,13 +299,11 @@ public JobResult ProcessJob(ManagementJobConfiguration config) } } - public string AddCertificate(string certificateContents, string privateKeyPassword, string cryptoProvider) + public ResultObject AddCertificate(string certificateContents, string privateKeyPassword, string cryptoProvider) { try { - string newThumbprint = string.Empty; - - _logger.LogTrace("Attempting to execute PS function (Add-KFCertificateToStore)"); + _logger.LogTrace("Attempting to execute PS function (Add-KeyfactorCertificate)"); // Mandatory parameters var parameters = new Dictionary @@ -297,20 +317,17 @@ public string AddCertificate(string certificateContents, string privateKeyPasswo if (!string.IsNullOrEmpty(cryptoProvider)) { parameters.Add("CryptoServiceProvider", cryptoProvider); } _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); - _logger.LogTrace("Returned from executing PS function (Add-KFCertificateToStore)"); + _logger.LogTrace("Returned from executing PS function (Add-KeyfactorCertificate)"); - // This should return the thumbprint of the certificate - if (_results != null && _results.Count > 0) - { - newThumbprint = _results[0].ToString(); - _logger.LogTrace($"Added certificate to store {_storePath}, returned with the thumbprint {newThumbprint}"); - } - else + ResultObject result = ResultObject.FromPSResults(_results); + _logger.LogTrace($"Add-KeyfactorCertificate returned Status={result.Status}, Code={result.Code}, Step={result.Step}, Thumbprint='{result.Thumbprint}'"); + + if (!result.IsSuccess && !string.IsNullOrEmpty(result.ErrorMessage)) { - _logger.LogTrace("No results were returned. There could have been an error while adding the certificate. Look in the trace logs for PowerShell information."); + _logger.LogWarning($"Add-KeyfactorCertificate error: {result.ErrorMessage}"); } - return newThumbprint; + return result; } catch (Exception ex) { diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index fc0c1681..8569e782 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Management.Automation; +using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -122,11 +123,32 @@ public JobResult ProcessJob(ManagementJobConfiguration config) // Add Certificate to Cert Store try { - string newThumbprint = AddCertificate(certificateContents, privateKeyPassword, cryptoProvider); - _logger.LogTrace($"Completed adding the certificate to the store"); + ResultObject addResult = AddCertificate(certificateContents, privateKeyPassword, cryptoProvider); + _logger.LogTrace($"Completed adding the certificate to the store. Status={addResult.Status}, Code={addResult.Code}, Step={addResult.Step}"); + + if (!addResult.IsSuccess) + { + string detail = !string.IsNullOrEmpty(addResult.ErrorMessage) + ? addResult.ErrorMessage + : addResult.Message; + + string addFailureMessage = + $"Add certificate to store '{_storePath}' failed at step '{addResult.Step}' (code {addResult.Code}): {detail}"; + + _logger.LogError(addFailureMessage); + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = _jobHistoryID, + FailureMessage = addFailureMessage + }; + } + + string newThumbprint = addResult.Thumbprint; // Bind Certificate to SQL Instance - if (newThumbprint != null) + if (!string.IsNullOrEmpty(newThumbprint)) { if (WinSqlBinding.BindSQLCertificate(_psHelper, SQLInstanceNames, newThumbprint, RenewalThumbprint, _storePath, RestartSQLService)) { @@ -148,6 +170,15 @@ public JobResult ProcessJob(ManagementJobConfiguration config) } } + else + { + complete = new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = _jobHistoryID, + FailureMessage = $"Add-KeyfactorCertificate reported Success but did not return a thumbprint. Unable to bind certificate to SQL Instance." + }; + } } catch (Exception ex) { @@ -262,13 +293,11 @@ public JobResult RemoveCertificate(string thumbprint) } } - public string AddCertificate(string certificateContents, string privateKeyPassword, string cryptoProvider) + public ResultObject AddCertificate(string certificateContents, string privateKeyPassword, string cryptoProvider) { try { - string newThumbprint = string.Empty; - - _logger.LogTrace("Attempting to execute PS function (Add-KFCertificateToStore)"); + _logger.LogTrace("Attempting to execute PS function (Add-KeyfactorCertificate)"); // Mandatory parameters var parameters = new Dictionary @@ -284,18 +313,15 @@ public string AddCertificate(string certificateContents, string privateKeyPasswo _results = _psHelper.ExecutePowerShell("Add-KeyfactorCertificate", parameters); _logger.LogTrace("Returned from executing PS function (Add-KeyfactorCertificate)"); - // This should return the thumbprint of the certificate - if (_results != null && _results.Count > 0) - { - newThumbprint = _results[0].ToString(); - _logger.LogTrace($"Added certificate to store {_storePath}, returned with the thumbprint {newThumbprint}"); - } - else + ResultObject result = ResultObject.FromPSResults(_results); + _logger.LogTrace($"Add-KeyfactorCertificate returned Status={result.Status}, Code={result.Code}, Step={result.Step}, Thumbprint='{result.Thumbprint}'"); + + if (!result.IsSuccess && !string.IsNullOrEmpty(result.ErrorMessage)) { - _logger.LogTrace("No results were returned. There could have been an error while adding the certificate. Look in the trace logs for PowerShell information."); + _logger.LogWarning($"Add-KeyfactorCertificate error: {result.ErrorMessage}"); } - return newThumbprint; + return result; } catch (Exception ex) { diff --git a/IISU/Models/ResultObject.cs b/IISU/Models/ResultObject.cs index 6aa75aab..407e1957 100644 --- a/IISU/Models/ResultObject.cs +++ b/IISU/Models/ResultObject.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Management.Automation; using System.Text; using System.Threading.Tasks; @@ -8,11 +11,125 @@ namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models { public class ResultObject { + public const string StatusSuccess = "Success"; + public const string StatusWarning = "Warning"; + public const string StatusSkipped = "Skipped"; + public const string StatusError = "Error"; + public string Status { get; set; } public int Code { get; set; } public string Step { get; set; } public string Message { get; set; } public string ErrorMessage { get; set; } - public Dictionary Details { get; set; } + public Dictionary Details { get; set; } = new Dictionary(); + + ///

+ /// True when Status is Success (case-insensitive). + /// + public bool IsSuccess => + string.Equals(Status, StatusSuccess, StringComparison.OrdinalIgnoreCase); + + /// + /// Convenience accessor for the Thumbprint value written into Details by + /// scripts such as Add-KeyfactorCertificate. Returns an empty string when + /// missing. + /// + public string Thumbprint => + Details != null && Details.TryGetValue("Thumbprint", out var v) && v != null + ? v.ToString() + : string.Empty; + + /// + /// Builds a ResultObject from a PowerShell PSObject that follows the + /// New-KeyfactorResult contract (Status, Code, Step, Message, + /// ErrorMessage, Details). Missing properties become sensible defaults. + /// + public static ResultObject FromPSObject(PSObject psObject) + { + var result = new ResultObject + { + Status = StatusError, + Code = -1, + Step = string.Empty, + Message = string.Empty, + ErrorMessage = string.Empty, + Details = new Dictionary() + }; + + if (psObject == null) + { + result.ErrorMessage = "PowerShell returned a null result object."; + return result; + } + + result.Status = psObject.Properties["Status"]?.Value as string ?? StatusError; + result.Step = psObject.Properties["Step"]?.Value as string ?? string.Empty; + result.Message = psObject.Properties["Message"]?.Value as string ?? string.Empty; + result.ErrorMessage = psObject.Properties["ErrorMessage"]?.Value as string ?? string.Empty; + + var codeValue = psObject.Properties["Code"]?.Value; + if (codeValue is int intCode) + { + result.Code = intCode; + } + else if (codeValue != null && int.TryParse(codeValue.ToString(), out var parsed)) + { + result.Code = parsed; + } + + var detailsValue = psObject.Properties["Details"]?.Value; + if (detailsValue is PSObject detailsPs && detailsPs.BaseObject is IDictionary dictBase) + { + CopyDictionary(dictBase, result.Details); + } + else if (detailsValue is IDictionary directDict) + { + CopyDictionary(directDict, result.Details); + } + + return result; + } + + /// + /// Builds a ResultObject from the first item of a PowerShell result + /// collection. When the collection is null or empty, returns an Error + /// ResultObject explaining that no result was produced. + /// + public static ResultObject FromPSResults(Collection results) + { + if (results == null || results.Count == 0 || results[0] == null) + { + return new ResultObject + { + Status = StatusError, + Code = -1, + Step = "CatchAll", + ErrorMessage = "PowerShell script returned no results.", + Details = new Dictionary() + }; + } + + return FromPSObject(results[0]); + } + + private static void CopyDictionary(IDictionary source, Dictionary target) + { + foreach (DictionaryEntry entry in source) + { + var key = entry.Key?.ToString(); + if (string.IsNullOrEmpty(key)) + { + continue; + } + + var value = entry.Value; + if (value is PSObject psValue) + { + value = psValue.BaseObject ?? psValue; + } + + target[key] = value; + } + } } } diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index be837487..0d447d4f 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -888,9 +888,9 @@ public static void ProcessPowerShellScriptEvent(object? sender, DataAddedEventAr { var msg = infoMessages[e.Index].MessageData?.ToString() ?? string.Empty; if (msg.StartsWith("[VERBOSE] ", StringComparison.Ordinal)) - _logger.LogTrace("{Message}", msg[10..]); + _logger.LogTrace($"{msg[10..]}"); else - _logger.LogInformation("INFO: {Message}", msg); + _logger.LogInformation($"{msg}"); } break; @@ -899,7 +899,7 @@ public static void ProcessPowerShellScriptEvent(object? sender, DataAddedEventAr if (warningMessages != null) { var warningMessage = warningMessages[e.Index]; - _logger.LogWarning($"WARN: {warningMessage.Message}"); + _logger.LogWarning($"{warningMessage.Message}"); } break; default: diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 index 1cf00d67..4234fcde 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1 @@ -1,128 +1,172 @@ -function Add-KeyfactorCertificate { - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$PrivateKeyPassword, - - [Parameter(Mandatory = $true)] - [string]$StoreName, - - [Parameter(Mandatory = $false)] - [string]$CryptoServiceProvider - ) - - try { - Write-Information "Entering PowerShell Script Add-KeyfactorCertificateToStore" - Write-Information "[VERBOSE] Add-KeyfactorCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" - - # Get the thumbprint of the passed in certificate - # Convert password to secure string if provided, otherwise use $null - $bytes = [System.Convert]::FromBase64String($Base64Cert) - $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } - - # Set the storage flags and get the certificate's thumbprint - $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor ` - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet - - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, $keyStorageFlags) - $thumbprint = $cert.Thumbprint - - if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } - - if ($CryptoServiceProvider) - { - # Test to see if CSP exists - if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) - { - Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system." - Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system." - return - } - - Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" - - # Create temporary file for the PFX - $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx" - [System.IO.File]::WriteAllBytes($tempPfx, [Convert]::FromBase64String($Base64Cert)) - - - # Execute certutil based on whether a private key password was supplied - try { - # Start building certutil arguments - $arguments = @('-f') - - if ($PrivateKeyPassword) { - Write-Information "[VERBOSE] Has a private key" - $arguments += '-p' - $arguments += $PrivateKeyPassword - } - - if ($CryptoServiceProvider) { - Write-Information "[VERBOSE] Has a CryptoServiceProvider: $CryptoServiceProvider" - $arguments += '-csp' - $arguments += $CryptoServiceProvider - } - - $arguments += '-importpfx' - $arguments += $StoreName - $arguments += $tempPfx - - # Quote any arguments with spaces - $argLine = ($arguments | ForEach-Object { - if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } - }) -join ' ' - - Write-Information "[VERBOSE] Running certutil with arguments: $argLine" - - # Setup process execution - $processInfo = New-Object System.Diagnostics.ProcessStartInfo - $processInfo.FileName = "certutil.exe" - $processInfo.Arguments = $argLine.Trim() - $processInfo.RedirectStandardOutput = $true - $processInfo.RedirectStandardError = $true - $processInfo.UseShellExecute = $false - $processInfo.CreateNoWindow = $true - - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $processInfo - - $process.Start() | Out-Null - - $stdOut = $process.StandardOutput.ReadToEnd() - $stdErr = $process.StandardError.ReadToEnd() - - $process.WaitForExit() - - if ($process.ExitCode -ne 0) { - throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" - } - } catch { - Write-Error "ERROR: $_" - } finally { - if (Test-Path $tempPfx) { - Remove-Item $tempPfx -Force - } - } - - } else { - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" - Write-Information "Store '$StoreName' is open." - - # Open store with read/write, and don't create the store if it doesn't exist - $openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite -bor ` - [System.Security.Cryptography.X509Certificates.OpenFlags]::OpenExistingOnly - $certStore.Open($openFlags) - $certStore.Add($cert) - $certStore.Close(); - Write-Information "Store '$StoreName' is closed." - } - - Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." - return $thumbprint - } catch { - Write-Error "An error occurred: $_" - return $null - } -} \ No newline at end of file +function Add-KeyfactorCertificate { + param ( + [Parameter(Mandatory = $true)] + [string]$Base64Cert, + + [Parameter(Mandatory = $false)] + [string]$PrivateKeyPassword, + + [Parameter(Mandatory = $true)] + [string]$StoreName, + + [Parameter(Mandatory = $false)] + [string]$CryptoServiceProvider + ) + + $thumbprint = $null + $tempPfx = $null + + try { + Write-Information "Entering PowerShell Script Add-KeyfactorCertificate" + Write-Information "[VERBOSE] Add-KeyfactorCertificate - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider'" + + # --- Step: LoadPfx --------------------------------------------------- + # Parse the PFX and extract the thumbprint. This validates the payload + # and the private key password before we attempt any store operations. + try { + $bytes = [System.Convert]::FromBase64String($Base64Cert) + $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } + + $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor ` + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet + + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, $keyStorageFlags) + $thumbprint = $cert.Thumbprint + } + catch { + $msg = "Failed to parse PFX payload (invalid Base64, corrupt PFX, or wrong password): $($_.Exception.Message)" + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 501 -Step LoadPfx -ErrorMessage $msg + } + + if (-not $thumbprint) { + $msg = "PFX parsed but no thumbprint was produced. The PFX may be invalid or the password is incorrect." + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 501 -Step LoadPfx -ErrorMessage $msg + } + + # --- Step: ValidateCSP ----------------------------------------------- + # If the caller requested a specific CSP, verify it exists on the + # target system BEFORE attempting the import. When it is missing, + # enumerate the installed CSPs so the operator sees what is available. + if ($CryptoServiceProvider) { + if (-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) { + $available = @() + try { $available = @(Get-CryptoProviders) } catch { } + + $availableText = if ($available.Count -gt 0) { ($available -join ', ') } else { '(none enumerated)' } + $msg = "The requested Crypto Service Provider '$CryptoServiceProvider' was not found on the target system. Available CSPs: $availableText" + + Write-Warning $msg + return New-KeyfactorResult -Status Error -Code 510 -Step ValidateCSP ` + -ErrorMessage $msg ` + -Details @{ + RequestedCSP = $CryptoServiceProvider + AvailableCSPs = $available + Thumbprint = $thumbprint + } + } + + # --- Step: CertUtilImport --------------------------------------- + # Import via certutil.exe so the requested CSP is honoured. + Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" + + $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx" + [System.IO.File]::WriteAllBytes($tempPfx, $bytes) + + $arguments = @('-f') + if ($PrivateKeyPassword) { + Write-Information "[VERBOSE] Has a private key" + $arguments += @('-p', $PrivateKeyPassword) + } + Write-Information "[VERBOSE] Has a CryptoServiceProvider: $CryptoServiceProvider" + $arguments += @('-csp', $CryptoServiceProvider, '-importpfx', $StoreName, $tempPfx) + + # Quote any argument containing whitespace + $argLine = ($arguments | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' + + Write-Information "[VERBOSE] Running certutil with arguments: $argLine" + + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "certutil.exe" + $processInfo.Arguments = $argLine.Trim() + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + + try { + [void]$process.Start() + } + catch { + $msg = "Failed to launch certutil.exe: $($_.Exception.Message)" + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 521 -Step CertUtilImport ` + -ErrorMessage $msg ` + -Details @{ Thumbprint = $thumbprint } + } + + $stdOut = $process.StandardOutput.ReadToEnd() + $stdErr = $process.StandardError.ReadToEnd() + $process.WaitForExit() + + if ($process.ExitCode -ne 0) { + $msg = "certutil exited with code $($process.ExitCode) while importing PFX with CSP '$CryptoServiceProvider'. StdErr: $stdErr StdOut: $stdOut" + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 520 -Step CertUtilImport ` + -ErrorMessage $msg ` + -Details @{ + ExitCode = $process.ExitCode + StdOut = $stdOut + StdErr = $stdErr + Thumbprint = $thumbprint + } + } + } + else { + # --- Step: ImportCertificate (managed API path) ----------------- + try { + $certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $StoreName, "LocalMachine" + Write-Information "Store '$StoreName' is open." + + $openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite -bor ` + [System.Security.Cryptography.X509Certificates.OpenFlags]::OpenExistingOnly + + $certStore.Open($openFlags) + $certStore.Add($cert) + $certStore.Close() + Write-Information "Store '$StoreName' is closed." + } + catch { + $msg = "Failed to open/write certificate store '$StoreName' on LocalMachine: $($_.Exception.Message)" + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 530 -Step ImportCertificate ` + -ErrorMessage $msg ` + -Details @{ Thumbprint = $thumbprint } + } + } + + Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." + + return New-KeyfactorResult -Status Success -Code 0 -Step ImportCertificate ` + -Message "Certificate '$thumbprint' added to store '$StoreName'." ` + -Details @{ Thumbprint = $thumbprint } + } + catch { + $msg = "Unexpected error in Add-KeyfactorCertificate: $($_.Exception.Message)" + Write-Error $msg + return New-KeyfactorResult -Status Error -Code 300 -Step CatchAll ` + -ErrorMessage $msg ` + -Details @{ Thumbprint = $thumbprint } + } + finally { + if ($tempPfx -and (Test-Path $tempPfx)) { + Remove-Item $tempPfx -Force -ErrorAction SilentlyContinue + } + } +} diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 index 9bfec511..832f1d8b 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 @@ -27,6 +27,13 @@ # 300 Error Unknown or unhandled exception # 400 Error Invalid Ssl Flag bit combination +# Codes reserved for Add-KeyfactorCertificate / import path +# 501 Error PFX payload could not be decoded or password was incorrect +# 510 Error Requested CSP was not found on the target system +# 520 Error certutil.exe returned a non-zero exit code +# 521 Error certutil.exe could not be started +# 530 Error Windows certificate store could not be opened for write + function New-KeyfactorResult { param( [ValidateSet("Success", "Warning", "Error", "Skipped")] diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index 25ebe0f2..02e61cb7 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -67,7 +67,6 @@ - diff --git a/WindowsCertStore.UnitTests/AddKeyfactorCertificateScriptTests.cs b/WindowsCertStore.UnitTests/AddKeyfactorCertificateScriptTests.cs new file mode 100644 index 00000000..14adb816 --- /dev/null +++ b/WindowsCertStore.UnitTests/AddKeyfactorCertificateScriptTests.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Runtime.InteropServices; +using Keyfactor.Extensions.Orchestrator.WindowsCertStore; +using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; +using Microsoft.PowerShell; + +namespace WindowsCertStore.UnitTests +{ + /// + /// Invokes the Add-KeyfactorCertificate PowerShell function in-process and + /// verifies it always returns a New-KeyfactorResult-shaped object with a + /// meaningful Status/Code/Step even in failure paths (bad PFX payload, + /// missing CSP, etc.). + /// + /// These tests exercise the real .ps1 files that ship in the extension so + /// regressions in the script surface immediately. + /// + /// Windows-only because the CSP validation path shells out to certutil.exe. + /// + public class AddKeyfactorCertificateScriptTests + { + // A CSP name that will not exist on any real system. + private const string NonExistentCsp = "___KeyfactorTest_DoesNotExist_CSP___"; + + [Fact] + public void BadBase64_ReturnsError_LoadPfx_Code501() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; // Extension is Windows-only. + } + + using var ps = CreateSessionWithScripts(); + + ps.AddCommand("Add-KeyfactorCertificate") + .AddParameter("Base64Cert", "this-is-not-valid-base64!!") + .AddParameter("StoreName", "My"); + + var result = InvokeSingleResult(ps); + + AssertStatus(result, expectedStatus: "Error", expectedStep: "LoadPfx", expectedCode: 501); + var errorMessage = (string)result.Properties["ErrorMessage"].Value; + Assert.Contains("PFX", errorMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ValidBase64ButNotAPfx_ReturnsError_LoadPfx_Code501() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + using var ps = CreateSessionWithScripts(); + + // Well-formed Base64, but decoded bytes are not a PFX. + string junkBase64 = Convert.ToBase64String(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }); + + ps.AddCommand("Add-KeyfactorCertificate") + .AddParameter("Base64Cert", junkBase64) + .AddParameter("StoreName", "My"); + + var result = InvokeSingleResult(ps); + + AssertStatus(result, expectedStatus: "Error", expectedStep: "LoadPfx", expectedCode: 501); + } + + [Fact] + public void MissingCsp_ReturnsError_ValidateCsp_Code510_WithAvailableList() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + using var ps = CreateSessionWithScripts(); + + // Build a self-signed PFX in-memory so LoadPfx succeeds and we hit ValidateCSP. + string pfxBase64 = CreateSelfSignedPfxBase64(password: string.Empty, out string _); + + ps.AddCommand("Add-KeyfactorCertificate") + .AddParameter("Base64Cert", pfxBase64) + .AddParameter("StoreName", "My") + .AddParameter("CryptoServiceProvider", NonExistentCsp); + + var result = InvokeSingleResult(ps); + + AssertStatus(result, expectedStatus: "Error", expectedStep: "ValidateCSP", expectedCode: 510); + + var errorMessage = (string)result.Properties["ErrorMessage"].Value; + Assert.Contains(NonExistentCsp, errorMessage); + Assert.Contains("Available CSPs:", errorMessage); + + // Verify the C# parser lifts these through to the ResultObject. + ResultObject parsed = ResultObject.FromPSObject(result); + Assert.False(parsed.IsSuccess); + Assert.Equal(510, parsed.Code); + Assert.Equal("ValidateCSP", parsed.Step); + Assert.Equal(NonExistentCsp, parsed.Details["RequestedCSP"]); + Assert.False(string.IsNullOrEmpty(parsed.Thumbprint)); + } + + // ---- helpers --------------------------------------------------------- + + /// + /// Creates a PowerShell runspace with the Keyfactor.WinCert.Common + /// module imported. Uses Import-Module against the .psm1 which + /// explicitly injects exported functions into the runspace's global + /// scope. This is the same mechanism the JEA endpoint uses in + /// production, so it exercises the module surface honestly. + /// + private static PowerShell CreateSessionWithScripts() + { + string scriptsRoot = PSHelper.FindScriptsDirectory( + AppDomain.CurrentDomain.BaseDirectory, "PowerShell"); + + Assert.False(string.IsNullOrEmpty(scriptsRoot), + "Could not locate the PowerShell scripts folder in the test output directory."); + + string modulePath = Path.Combine( + scriptsRoot, "Keyfactor.WinCert.Common", "Keyfactor.WinCert.Common.psm1"); + + Assert.True(File.Exists(modulePath), + $"Keyfactor.WinCert.Common.psm1 was not found at expected path: {modulePath}"); + + var iss = InitialSessionState.CreateDefault(); + iss.ExecutionPolicy = ExecutionPolicy.Bypass; + var runspace = RunspaceFactory.CreateRunspace(iss); + runspace.Open(); + + var ps = PowerShell.Create(); + ps.Runspace = runspace; + + // Import-Module places exported functions into the runspace's + // global scope so subsequent AddCommand("Add-KeyfactorCertificate") + // calls resolve correctly. + ps.AddCommand("Import-Module") + .AddParameter("Name", modulePath) + .AddParameter("Force") + .Invoke(); + + if (ps.HadErrors) + { + string errors = string.Join("; ", ps.Streams.Error.Select(e => e.ToString())); + throw new InvalidOperationException($"Failed to import module '{modulePath}': {errors}"); + } + + ps.Commands.Clear(); + ps.Streams.ClearStreams(); + return ps; + } + + private static PSObject InvokeSingleResult(PowerShell ps) + { + var results = ps.Invoke(); + Assert.NotNull(results); + Assert.True(results.Count >= 1, + $"Expected the script to return a result object. HadErrors={ps.HadErrors}. " + + $"Errors: {string.Join("; ", ps.Streams.Error.Select(e => e.ToString()))}"); + + return results[0]; + } + + private static void AssertStatus(PSObject result, string expectedStatus, string expectedStep, int expectedCode) + { + Assert.NotNull(result); + Assert.Equal(expectedStatus, (string)result.Properties["Status"].Value); + Assert.Equal(expectedStep, (string)result.Properties["Step"].Value); + Assert.Equal(expectedCode, (int)result.Properties["Code"].Value); + } + + /// + /// Builds a self-signed cert with an exportable RSA key and returns + /// the PFX bytes as a Base64 string. Used to exercise script paths + /// that require a parseable PFX. + /// + private static string CreateSelfSignedPfxBase64(string password, out string thumbprint) + { + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=KeyfactorUnitTest", + rsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + + using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); + thumbprint = cert.Thumbprint; + + byte[] pfxBytes = cert.Export( + System.Security.Cryptography.X509Certificates.X509ContentType.Pfx, + password ?? string.Empty); + + return Convert.ToBase64String(pfxBytes); + } + } +} diff --git a/WindowsCertStore.UnitTests/ResultObjectTests.cs b/WindowsCertStore.UnitTests/ResultObjectTests.cs new file mode 100644 index 00000000..f16ad8e4 --- /dev/null +++ b/WindowsCertStore.UnitTests/ResultObjectTests.cs @@ -0,0 +1,208 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; + +namespace WindowsCertStore.UnitTests +{ + public class ResultObjectTests + { + [Fact] + public void FromPSObject_Null_ReturnsErrorWithMessage() + { + ResultObject result = ResultObject.FromPSObject(null!); + + Assert.False(result.IsSuccess); + Assert.Equal(ResultObject.StatusError, result.Status); + Assert.Equal(-1, result.Code); + Assert.Equal("PowerShell returned a null result object.", result.ErrorMessage); + Assert.NotNull(result.Details); + Assert.Empty(result.Details); + } + + [Fact] + public void FromPSObject_SuccessWithThumbprintInDetails_Populates() + { + PSObject ps = BuildPSObject( + status: "Success", + code: 0, + step: "ImportCertificate", + message: "Certificate '1234ABCD' added.", + errorMessage: string.Empty, + details: new Hashtable { { "Thumbprint", "1234ABCD" } }); + + ResultObject result = ResultObject.FromPSObject(ps); + + Assert.True(result.IsSuccess); + Assert.Equal("Success", result.Status); + Assert.Equal(0, result.Code); + Assert.Equal("ImportCertificate", result.Step); + Assert.Equal("Certificate '1234ABCD' added.", result.Message); + Assert.Equal("1234ABCD", result.Thumbprint); + } + + [Fact] + public void FromPSObject_CspNotFoundError_ExposesAvailableCsps() + { + var available = new object[] { "Microsoft Software Key Storage Provider", "Microsoft Platform Crypto Provider" }; + + PSObject ps = BuildPSObject( + status: "Error", + code: 510, + step: "ValidateCSP", + message: string.Empty, + errorMessage: "The requested Crypto Service Provider 'FooCSP' was not found on the target system. Available CSPs: Microsoft Software Key Storage Provider, Microsoft Platform Crypto Provider", + details: new Hashtable + { + { "RequestedCSP", "FooCSP" }, + { "AvailableCSPs", available }, + { "Thumbprint", "ABCDEF12" } + }); + + ResultObject result = ResultObject.FromPSObject(ps); + + Assert.False(result.IsSuccess); + Assert.Equal("Error", result.Status); + Assert.Equal(510, result.Code); + Assert.Equal("ValidateCSP", result.Step); + Assert.Contains("FooCSP", result.ErrorMessage); + Assert.Contains("Microsoft Software Key Storage Provider", result.ErrorMessage); + + Assert.Equal("FooCSP", result.Details["RequestedCSP"]); + Assert.Same(available, result.Details["AvailableCSPs"]); + Assert.Equal("ABCDEF12", result.Thumbprint); + } + + [Fact] + public void FromPSObject_CodeAsString_ParsesToInt() + { + PSObject ps = BuildPSObject( + status: "Error", + codeRaw: "520", + step: "CertUtilImport", + message: string.Empty, + errorMessage: "certutil exited with code 2148073494", + details: null); + + ResultObject result = ResultObject.FromPSObject(ps); + + Assert.Equal(520, result.Code); + Assert.Equal("CertUtilImport", result.Step); + } + + [Fact] + public void FromPSObject_MissingProperties_UsesDefaults() + { + var ps = new PSObject(); + ps.Properties.Add(new PSNoteProperty("Status", "Error")); + + ResultObject result = ResultObject.FromPSObject(ps); + + Assert.False(result.IsSuccess); + Assert.Equal("Error", result.Status); + Assert.Equal(-1, result.Code); + Assert.Equal(string.Empty, result.Step); + Assert.Equal(string.Empty, result.Message); + Assert.Equal(string.Empty, result.ErrorMessage); + Assert.Empty(result.Details); + Assert.Equal(string.Empty, result.Thumbprint); + } + + [Fact] + public void IsSuccess_CaseInsensitive() + { + var result = new ResultObject { Status = "success" }; + Assert.True(result.IsSuccess); + } + + [Fact] + public void Thumbprint_MissingDetail_ReturnsEmpty() + { + var result = new ResultObject + { + Status = "Success", + Details = new Dictionary() + }; + + Assert.Equal(string.Empty, result.Thumbprint); + } + + [Fact] + public void FromPSResults_NullCollection_ReturnsCatchAllError() + { + ResultObject result = ResultObject.FromPSResults(null!); + + Assert.False(result.IsSuccess); + Assert.Equal(ResultObject.StatusError, result.Status); + Assert.Equal("CatchAll", result.Step); + Assert.Contains("no results", result.ErrorMessage); + } + + [Fact] + public void FromPSResults_EmptyCollection_ReturnsCatchAllError() + { + ResultObject result = ResultObject.FromPSResults(new Collection()); + + Assert.False(result.IsSuccess); + Assert.Equal(ResultObject.StatusError, result.Status); + Assert.Equal("CatchAll", result.Step); + } + + [Fact] + public void FromPSResults_FirstItemMapped() + { + var collection = new Collection + { + BuildPSObject( + status: "Success", + code: 0, + step: "ImportCertificate", + message: "ok", + errorMessage: string.Empty, + details: new Hashtable { { "Thumbprint", "AAAABBBB" } }) + }; + + ResultObject result = ResultObject.FromPSResults(collection); + + Assert.True(result.IsSuccess); + Assert.Equal("AAAABBBB", result.Thumbprint); + } + + private static PSObject BuildPSObject( + string status, + int code, + string step, + string message, + string errorMessage, + Hashtable? details) + { + var ps = new PSObject(); + ps.Properties.Add(new PSNoteProperty("Status", status)); + ps.Properties.Add(new PSNoteProperty("Code", code)); + ps.Properties.Add(new PSNoteProperty("Step", step)); + ps.Properties.Add(new PSNoteProperty("Message", message)); + ps.Properties.Add(new PSNoteProperty("ErrorMessage", errorMessage)); + ps.Properties.Add(new PSNoteProperty("Details", details)); + return ps; + } + + private static PSObject BuildPSObject( + string status, + object codeRaw, + string step, + string message, + string errorMessage, + Hashtable? details) + { + var ps = new PSObject(); + ps.Properties.Add(new PSNoteProperty("Status", status)); + ps.Properties.Add(new PSNoteProperty("Code", codeRaw)); + ps.Properties.Add(new PSNoteProperty("Step", step)); + ps.Properties.Add(new PSNoteProperty("Message", message)); + ps.Properties.Add(new PSNoteProperty("ErrorMessage", errorMessage)); + ps.Properties.Add(new PSNoteProperty("Details", details)); + return ps; + } + } +} From e38da14f3cc3344106eceef7b96661d30f6767ac Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 6 Jul 2026 13:40:52 -0700 Subject: [PATCH 41/53] Updated PS scripts to get CSPs from registry not certutil. --- CHANGELOG.md | 5 +- .../Private/Get-CryptoProviders.ps1 | 50 +++++++++++++++---- .../Private/Validate-CryptoProvider.ps1 | 5 +- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c315e1..3d2bb222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Adding JEA Support for local PowerShell execution. This will allow for more secure execution of the extension when running in a local PowerShell Runspace. To utilize this feature, you will need to create a JEA endpoint on the target server and specify the endpoint name as a new parameter in the specific Cert Store definition. Refer to the README for more details. * .NET6 assemblies are no longer supported. * Fixed a problem when passing a bad CSP, the job was reporting successful, but did not actually add/bind the certificate. +* Enhanced Crypto Service Provider (CSP) discovery and validation to work correctly on localized Windows installations. Customers running Traditional Chinese Windows (zh-TW / CP950) reported that certificate add jobs were failing with "Crypto Service Provider ... is either invalid or not found on this system." even when the requested CSP was installed. The root cause was that `Get-CryptoProviders` parsed `certutil -csplist` output and matched on the literal English label "Provider Name:", which is translated on non en-US Windows, causing the enumerated provider list to come back empty and every CSP name to fail validation. Provider enumeration now reads from the culture-invariant registry hive `HKLM:\SOFTWARE\Microsoft\Cryptography\Defaults\Provider` (with a code-page-safe `certutil` fallback for hardened systems where the registry hive is unavailable), and CSP name matching in `Validate-CryptoProvider` now uses ordinal, case-insensitive comparison so results are unaffected by the current thread culture (Turkish-I folding, full-/half-width character folding, etc.). 3.0.2 @@ -22,14 +23,14 @@ * Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags * Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags * Added SSL Flag validation to make sure the bit flag is correct. These are the valid bit flags for the version of Windows: - ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): + ### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5): * 0 No SNI * 1 Use SNI * 2 Use Centralized SSL certificate store. - ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): + ### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0)### Windows Server 2016 (IIS 10.0): * 0 No SNI * 1 Use SNI diff --git a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 index bf04e586..0f215308 100644 --- a/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 +++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1 @@ -1,20 +1,50 @@ function Get-CryptoProviders { - # Retrieves the list of available Crypto Service Providers using certutil + # Retrieves the list of available Crypto Service Providers. + # + # Preferred source is the registry because CSP names are stored as + # culture-invariant subkey names under + # HKLM:\SOFTWARE\Microsoft\Cryptography\Defaults\Provider + try { - Write-Information "[VERBOSE] Retrieving Crypto Service Providers using certutil..." - $certUtilOutput = certutil -csplist - - # Parse the output to extract CSP names + Write-Information "[VERBOSE] Retrieving Crypto Service Providers from registry..." + + $regPath = 'HKLM:\SOFTWARE\Microsoft\Cryptography\Defaults\Provider' $cspInfoList = @() - foreach ($line in $certUtilOutput) { - if ($line -match "Provider Name:") { - $cspName = ($line -split ":")[1].Trim() - $cspInfoList += $cspName + + if (Test-Path -LiteralPath $regPath) { + $cspInfoList = @( + Get-ChildItem -LiteralPath $regPath -ErrorAction Stop | + Select-Object -ExpandProperty PSChildName + ) + } + + if ($cspInfoList.Count -eq 0) { + Write-Information "[VERBOSE] Registry enumeration returned no CSPs; falling back to certutil." + + $prevOutEncoding = [Console]::OutputEncoding + try { + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + $certUtilOutput = & certutil.exe -csplist 2>&1 + } finally { + [Console]::OutputEncoding = $prevOutEncoding + } + + # The "Provider Name:" label is localized, so match on the structural + # shape of the line instead of the English text: an un-indented + # "