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
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/.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..30a276f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,21 @@
+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.
+* 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
+
+* 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
+
* 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.
@@ -9,38 +23,44 @@
* 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 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.
+ * 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.
+
+ * 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.
+
+ * 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.
@@ -51,23 +71,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
@@ -77,6 +99,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
@@ -84,9 +107,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.
@@ -96,22 +121,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.
@@ -119,66 +149,79 @@
* 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/ClientPSCertStoreReEnrollment.cs b/IISU/ClientPSCertStoreReEnrollment.cs
index da9adc2b..1c7cc02c 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)
@@ -399,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/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs
index 546640b8..c540188c 100644
--- a/IISU/ImplementedStoreTypes/Win/Inventory.cs
+++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs
@@ -81,6 +81,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn
settings.IncludePortInSPN = jobProperties.SpnPortFlag;
settings.ServerUserName = serverUserName;
settings.ServerPassword = serverPassword;
+ settings.JEAEndpointName = jobProperties.JEAEndpointName;
_logger.LogTrace($"Querying Window certificate in store: {storePath}");
inventoryItems = QueryWinCertCertificates(settings, storePath);
@@ -109,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
@@ -126,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))
+ using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName))
{
ps.Initialize();
@@ -135,7 +136,7 @@ public List QueryWinCertCertificates(RemoteSettings settin
{ "StoreName", StoreName }
};
- results = ps.ExecutePowerShell("Get-KFCertificates", 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)
@@ -147,8 +148,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..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
{
@@ -88,8 +89,9 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
string protocol = jobProperties?.WinRmProtocol;
string port = jobProperties?.WinRmPort;
bool includePortInSPN = (bool)jobProperties?.SpnPortFlag;
+ string jeaEndpoint = jobProperties?.JEAEndpointName ?? "";
- _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword);
+ _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint);
switch (_operationType)
{
@@ -125,7 +127,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
@@ -145,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
@@ -158,20 +160,34 @@ 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)
- {
- 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
@@ -212,8 +228,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/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/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/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs
index 65c1b68c..f5214850 100644
--- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs
+++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs
@@ -83,6 +83,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn
settings.IncludePortInSPN = jobProperties.SpnPortFlag;
settings.ServerUserName = serverUserName;
settings.ServerPassword = serverPassword;
+ settings.JEAEndpointName = jobProperties.JEAEndpointName;
_logger.LogTrace("Querying IIS Inventory..");
inventoryItems = QueryIISCertificates(settings);
@@ -127,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))
+ using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName))
{
ps.Initialize();
@@ -142,7 +143,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 b458584d..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
@@ -92,12 +91,13 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
string protocol = jobProperties?.WinRmProtocol;
string port = jobProperties?.WinRmPort;
bool includePortInSPN = (bool)jobProperties?.SpnPortFlag;
+ 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);
+ _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint);
_psHelper.Initialize();
@@ -121,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
@@ -200,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)
{
@@ -276,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
@@ -295,21 +316,18 @@ 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)
- {
- 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)
{
@@ -330,7 +348,7 @@ public void RemoveIISCertificate(string thumbprint)
{ "StoreName", _storePath }
};
- _psHelper.ExecutePowerShell("Remove-KFIISCertificateIfUnused", parameters);
+ _psHelper.ExecutePowerShell("Remove-KeyfactorIISCertificateIfUnused", parameters);
}
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/ImplementedStoreTypes/WinSQL/Inventory.cs b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs
index 765cf0a7..1eca1384 100644
--- a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs
+++ b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs
@@ -81,9 +81,10 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn
settings.IncludePortInSPN = jobProperties.SpnPortFlag;
settings.ServerUserName = serverUserName;
settings.ServerPassword = serverPassword;
+ 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);
@@ -109,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
@@ -121,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))
+ 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 2499dc5e..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;
@@ -91,7 +92,8 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
string protocol = jobProperties?.WinRmProtocol;
string port = jobProperties?.WinRmPort;
bool includePortInSPN = (bool)jobProperties?.SpnPortFlag;
-
+ string jeaEndpoint = jobProperties?.JEAEndpointName ?? "";
+
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, jeaEndpoint: jeaEndpoint);
_psHelper.Initialize();
@@ -121,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))
{
@@ -147,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)
{
@@ -206,7 +238,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
@@ -234,8 +266,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();
}
@@ -261,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
@@ -280,21 +310,18 @@ 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)
- {
- 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/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 a6ef63b0..d9d70747 100644
--- a/IISU/Models/JobProperties.cs
+++ b/IISU/Models/JobProperties.cs
@@ -51,5 +51,9 @@ public JobProperties()
[JsonProperty("RestartService")]
[DefaultValue(true)]
public bool RestartService { get; set; }
+
+ [JsonProperty("JEAEndpointName")]
+ [DefaultValue("")]
+ public string JEAEndpointName { get; set; } = "";
}
}
\ No newline at end of file
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 31b73af9..0d447d4f 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 string jeaEndpoint = "";
+ private bool useJea => !string.IsNullOrEmpty(jeaEndpoint);
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, string jeaEndpoint = "")
{
this.protocol = protocol.ToLower();
this.port = port;
@@ -103,6 +105,8 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN
ClientMachineName = clientMachineName;
this.serverUserName = serverUserName;
this.serverPassword = serverPassword;
+ this.isADFSStore = isADFSStore;
+ this.jeaEndpoint = jeaEndpoint;
_logger = LogHandler.GetClassLogger();
_logger.LogTrace("Entered PSHelper Constructor");
@@ -110,8 +114,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($"JEA Active: {this.useJea}");
+ _logger.LogTrace($"JEAEndpoint: {this.jeaEndpoint}");
_logger.LogTrace("Constructor Completed");
- this.isADFSStore = isADFSStore;
}
public void Initialize()
@@ -132,11 +137,20 @@ 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}");
+ 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();
@@ -146,8 +160,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()
@@ -158,16 +179,20 @@ 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}");
+ }
}
}
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")
{
@@ -202,16 +227,21 @@ private void InitializeRemoteSession()
_logger.LogTrace("Initializing WinRM connection");
try
{
- // Create the PSSessionOption object
- var sessionOption = new PSSessionOption
+ PS.AddCommand("New-PSSession")
+ .AddParameter("ComputerName", ClientMachineName)
+ .AddParameter("Port", port);
+
+ if (useSPN)
{
- IncludePortInSPN = useSPN
- };
+ var sessionOption = new PSSessionOption { IncludePortInSPN = true };
+ PS.AddParameter("SessionOption", sessionOption);
+ }
- PS.AddCommand("New-PSSession")
- .AddParameter("ComputerName", ClientMachineName)
- .AddParameter("Port", port)
- .AddParameter("SessionOption", sessionOption);
+ if (useJea)
+ {
+ PS.AddParameter("ConfigurationName", jeaEndpoint);
+ _logger.LogDebug($"JEA enabled - connecting to endpoint: {jeaEndpoint}");
+ }
if (protocol == "https")
{
@@ -228,17 +258,25 @@ 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");
}
}
- 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)
{
@@ -246,19 +284,74 @@ private void InitializeRemoteSession()
PS.Commands.Clear();
_logger.LogTrace("PS-Session established");
+ if (!useJea)
+ {
+ PS.AddCommand("Invoke-Command")
+ .AddParameter("Session", _PSSession)
+ .AddParameter("ScriptBlock", ScriptBlock.Create(LoadAllScripts(scriptFileLocation)));
+
+ 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.");
+
+ // 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
+ // 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(LoadAllScripts(scriptFileLocation)));
-
- var results = PS.Invoke();
- CheckErrors();
- _logger.LogTrace("Script loaded into remote session successfully.");
+ .AddParameter("ScriptBlock", ScriptBlock.Create("$global:InformationPreference = 'Continue'"));
+ PS.Invoke();
+ PS.Commands.Clear();
+ _logger.LogTrace("Remote session preference variables configured.");
}
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()
@@ -273,6 +366,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)
@@ -280,40 +380,61 @@ private void InitializeLocalSession()
adfsModuleImported = ImportAdfsModule();
}
- // Load all scripts
- _logger.LogTrace("Loading PowerShell scripts");
- var scriptFiles = GetScriptFiles(scriptFileLocation);
+ // 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 scriptFile in scriptFiles)
+ foreach (var moduleDir in moduleDirs)
{
- var fileName = Path.GetFileName(scriptFile);
- bool isAdfsScript = fileName.IndexOf("adfs", StringComparison.OrdinalIgnoreCase) >= 0;
+ var moduleName = Path.GetFileName(moduleDir);
+ var modulePsm1 = Path.Combine(moduleDir, $"{moduleName}.psm1");
- // Decide whether to load this script
- if (isAdfsScript)
+ if (!File.Exists(modulePsm1))
{
- if (this.isADFSStore)
- {
- if (!adfsModuleImported)
- {
- _logger.LogWarning($"Skipping ADFS script '{fileName}' - ADFS module not available");
- continue;
- }
+ _logger.LogTrace($"No .psm1 found in {moduleName}, skipping");
+ continue;
+ }
- _logger.LogTrace($"Loading ADFS script: {fileName}");
- }
- else
- {
- _logger.LogTrace($"Skipping ADFS script '{fileName}' - not an ADFS store");
- continue;
- }
+ _logger.LogTrace($"Importing module: {modulePsm1}");
+ PS.AddCommand("Import-Module")
+ .AddParameter("Name", modulePsm1)
+ .AddParameter("Force");
+ PS.Invoke();
+
+ if (PS.HadErrors)
+ {
+ _logger.LogError($"Errors importing {moduleName} module:");
+ foreach (var error in PS.Streams.Error)
+ _logger.LogError($" {error}");
+ PS.Streams.Error.Clear();
}
else
{
- _logger.LogTrace($"Loading script: {fileName}");
+ _logger.LogInformation($"{moduleName} module imported successfully.");
+ }
+ PS.Commands.Clear();
+ }
+
+ // 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;
}
- // Load the script
+ _logger.LogTrace($"Loading script: {fileName}");
try
{
PS.AddScript($". '{scriptFile}'");
@@ -323,16 +444,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)
@@ -461,94 +580,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()
{
@@ -735,14 +766,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
@@ -757,12 +786,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}"));
@@ -858,8 +886,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.ToString()}");
+ var msg = infoMessages[e.Index].MessageData?.ToString() ?? string.Empty;
+ if (msg.StartsWith("[VERBOSE] ", StringComparison.Ordinal))
+ _logger.LogTrace($"{msg[10..]}");
+ else
+ _logger.LogInformation($"{msg}");
}
break;
@@ -868,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:
@@ -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)
{
/*
@@ -1001,53 +1072,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}");
@@ -1094,8 +1119,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..1af65bf5
--- /dev/null
+++ b/IISU/PowerShell/Build/KeyfactorWinCert.pssc
@@ -0,0 +1,253 @@
+#
+# 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 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
+# 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.)
+#
+# 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
+#
+# 4. 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
+#
+# 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'
+#
+# 6. To update or remove the endpoint:
+#
+# Unregister-PSSessionConfiguration -Name 'keyfactor.wincert'
+# Restart-Service WinRM
+#
+# ============================================================
+# TESTING THE ENDPOINT
+# ============================================================
+#
+# # Connect and list available commands
+# $s = New-PSSession -ComputerName `
+# -ConfigurationName 'keyfactor.wincert' `
+# -Credential (Get-Credential)
+# Invoke-Command -Session $s -ScriptBlock { Get-Command }
+#
+# # 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
+#
+# ============================================================
+@{
+ 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
+ # ------------------------------------------------------------------
+ # 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 and the Group Managed Account that has been granted the necessary permissions (e.g. DOMAIN\{gMSA}).
+ #
+ #
+ # 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
+
+ # --- 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 ---
+ # 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
+ #
+ # 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.
+ #
+ # 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 = @{
+ # 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.Common/Keyfactor.WinCert.Common.psm1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1
new file mode 100644
index 00000000..7c5fb437
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1
@@ -0,0 +1,39 @@
+# 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"
+. "$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"
+. "$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.
+Export-ModuleMember -Function @(
+ # Public functions
+ 'New-KeyfactorResult',
+ 'Get-KeyfactorCertificates',
+ 'Add-KeyfactorCertificate',
+ '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/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
new file mode 100644
index 00000000..308811cb
--- /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-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 ""
+ }
+}
\ 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..0f215308
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Get-CryptoProviders.ps1
@@ -0,0 +1,57 @@
+function Get-CryptoProviders {
+ # 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 from registry..."
+
+ $regPath = 'HKLM:\SOFTWARE\Microsoft\Cryptography\Defaults\Provider'
+ $cspInfoList = @()
+
+ 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
+ # ": " line whose value is not a numeric provider type
+ # ("1 - PROV_RSA_FULL", etc.).
+ foreach ($line in $certUtilOutput) {
+ if ($line -match '^\S[^:]+:\s+(\S.+)$') {
+ $value = $Matches[1].Trim()
+ if ($value -notmatch '^\d+\s*-\s*\S') {
+ $cspInfoList += $value
+ }
+ }
+ }
+ }
+
+ if ($cspInfoList.Count -eq 0) {
+ throw "No Crypto Service Providers were found."
+ }
+
+ Write-Information "[VERBOSE] Retrieved the following CSPs:"
+ $cspInfoList | ForEach-Object { Write-Information "[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..8991ef91
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Private/Validate-CryptoProvider.ps1
@@ -0,0 +1,19 @@
+function Validate-CryptoProvider {
+ param (
+ [Parameter(Mandatory)]
+ [string]$ProviderName
+ )
+ Write-Information "[VERBOSE] Validating CSP: $ProviderName"
+
+ $availableProviders = Get-CryptoProviders
+
+ $trimmedProvider = $ProviderName.Trim()
+ if (-not ($availableProviders | Where-Object {
+ [string]::Equals($_.Trim(), $trimmedProvider, [System.StringComparison]::OrdinalIgnoreCase)
+ })) {
+
+ throw "Crypto Service Provider '$ProviderName' is either invalid or not found on this system."
+ }
+
+ 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
new file mode 100644
index 00000000..4234fcde
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Add-KeyfactorCertificate.ps1
@@ -0,0 +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
+ )
+
+ $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/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/Get-KeyfactorDiagnostics.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1
new file mode 100644
index 00000000..1061dbb9
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Get-KeyfactorDiagnostics.ps1
@@ -0,0 +1,178 @@
+function Get-KeyfactorDiagnostics {
+ [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/Public/Import-KeyfactorSignedCertificate.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1
new file mode 100644
index 00000000..8bc9ce5e
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/Import-KeyfactorSignedCertificate.ps1
@@ -0,0 +1,52 @@
+function Import-KeyfactorSignedCertificate {
+ 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-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."
+ $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."
+ }
+
+ # 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
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/Public/New-KeyfactorResult.ps1 b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1
new file mode 100644
index 00000000..832f1d8b
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1
@@ -0,0 +1,56 @@
+#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
+
+# 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")]
+ [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..ab67eec4
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc
@@ -0,0 +1,72 @@
+#
+# 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.
+ # 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',
+ 'Get-KeyfactorDiagnostics',
+ 'Add-KeyfactorCertificate',
+ 'Remove-KeyfactorCertificate',
+ 'New-KeyfactorResult',
+ 'New-KeyfactorODKGEnrollment',
+ 'Import-KeyfactorSignedCertificate',
+ 'Get-CertificateCSP',
+ 'Get-CertificateSAN'
+ )
+
+ # 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.
+ # 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\certreq.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/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1
new file mode 100644
index 00000000..6778330b
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Keyfactor.WinCert.IIS.psm1
@@ -0,0 +1,38 @@
+# 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"
+. "$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-KeyfactorIISCertificateIfUnused'
+)
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..5669de68
--- /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-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-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..08707bc8
--- /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-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-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..b5c5d347
--- /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-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)
+ }
+}
\ 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..dfe10f3d
--- /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-Information "[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..dddfe8d1
--- /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-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-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-Information "[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-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-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-KeyfactorIISCertificateIfUnused.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1
new file mode 100644
index 00000000..9a673fd9
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/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:\LocalMachine\$StoreName\$normalizedThumbprint" -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/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1 b/IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1
new file mode 100644
index 00000000..28a903d2
--- /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-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-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..7c488c56
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.IIS/RoleCapabilities/Keyfactor.WinCert.IIS.psrc
@@ -0,0 +1,50 @@
+#
+# 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',
+ 'Remove-KeyfactorIISCertificateIfUnused'
+ )
+
+ 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/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..ec2c5f1d
--- /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 service '$SQLServiceName' 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..ffd98116
--- /dev/null
+++ b/IISU/PowerShell/Keyfactor.WinCert.SQL/Private/Set-KeyfactorSQLCertificateBinding.ps1
@@ -0,0 +1,504 @@
+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 {
+ 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 restart completed but current status is: $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"
+ }
+ } 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/PowerShellScripts/WinADFSScripts.ps1 b/IISU/PowerShell/WinADFSScripts.ps1
similarity index 97%
rename from IISU/PowerShellScripts/WinADFSScripts.ps1
rename to IISU/PowerShell/WinADFSScripts.ps1
index 7f6ce516..d9917117 100644
--- a/IISU/PowerShellScripts/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/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1
deleted file mode 100644
index 2d3830b3..00000000
--- a/IISU/PowerShellScripts/WinCertScripts.ps1
+++ /dev/null
@@ -1,2226 +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
-
-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
- }
-}
-
-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'."
- }
-}
-
-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-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-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
- }
-}
-
-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
-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."
- }
-}
-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-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-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-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-ResultObject -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage
- }
-}
-function Remove-ExistingIISBinding {
- [CmdletBinding()]
- [OutputType([pscustomobject])]
- param (
- [Parameter(Mandatory = $true)]
- [string]$SiteName,
-
- [Parameter(Mandatory = $true)]
- [AllowEmptyString()]
- [string]$BindingInfo,
-
- [Parameter(Mandatory = $true)]
- [bool]$UseIISDrive
- )
-
- 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-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
- }
-}
-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-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-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-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'."
-
- 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
-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
-}
-
-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-Verbose "Instance: $instance"
- Write-Verbose "Full Instance: $fullInstance"
- Write-Verbose "Registry Location: $regLocation"
- Write-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
-}
-
-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-Verbose "Full instance name: $fullInstance"
- Write-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-Verbose "Current certificate thumbprint: $currentThumbprint"
- } else {
- Write-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-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"
- }
- elseif ($SqlServiceUser.StartsWith(".\")) {
- # Local account - convert to machine\user format
- $SqlServiceUser = "$env:COMPUTERNAME$($SqlServiceUser.Substring(1))"
- Write-Verbose "Normalized local account to: $SqlServiceUser"
- }
-
- Write-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-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-Verbose "Certificate uses CNG key storage"
- Write-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-Verbose "Searching for CNG private key in known locations..."
- foreach ($path in $possiblePaths) {
- Write-Verbose "Checking: $path"
- if (Test-Path $path) {
- $privKeyPath = Get-Item $path -ErrorAction Stop
- Write-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..."
-
- $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-Verbose "Found CNG private key at: $($privKeyPath.FullName)"
- break
- }
- }
- }
- }
-
- if ($privKeyPath) {
- Write-Information "Certificate uses CNG (Cryptography Next Generation) key storage"
- }
- }
- }
- catch {
- Write-Verbose "CNG key detection failed or not applicable: $_"
- Write-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..."
-
- try {
- if ($Cert.PrivateKey -and $Cert.PrivateKey.CspKeyContainerInfo) {
- $privKey = $Cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
- $keyPath = "$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\"
- $keyStorageType = "CAPI/CSP"
-
- Write-Verbose "CAPI/CSP key unique name: $privKey"
- Write-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: $_"
- }
- }
-
- 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-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-Verbose "Key storage type: $keyStorageType"
- Write-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"
- }
- 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-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-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..."
-
- 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"
-
- # 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-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-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-Verbose "Scheduled task registered: $taskName"
-
- # Wait for task to complete
- Write-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-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-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-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-Verbose "Stack trace: $($_.ScriptStackTrace)"
- return $false
- }
-}
-
-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"
-}
-
-function Get-SqlServiceName {
- param (
- [string]$InstanceName
- )
- if ($InstanceName -eq "MSSQLSERVER") {
- return "MSSQLSERVER" # Default instance
- } else {
- return "MSSQL`$$InstanceName" # Named instance (escape $ for PowerShell strings)
- }
-}
-
-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
-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-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-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."
- $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."
- }
-
- # 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: $_"
- }
-}
-#####
-
-# Shared Functions
-# Function to get SAN (Subject Alternative Names) from a certificate
-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
-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
-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 ""
- }
-}
-
-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: $_"
- }
-}
-
-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."
-}
-
-function Parse-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
-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)
- }
-}
-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
- }
- }
-}
-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
-function Get-IISManagementInfo {
- [CmdletBinding()]
- [OutputType([hashtable])]
- param (
- [Parameter(Mandatory = $true)]
- [string]$SiteName
- )
-
- $hasIISDrive = Ensure-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-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
- }
- }
-}
-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/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 43465b83..04f4056d 100644
--- a/IISU/RemoteSettings.cs
+++ b/IISU/RemoteSettings.cs
@@ -26,6 +26,7 @@ public class RemoteSettings
public string ServerUserName { get; set; }
public string ServerPassword { get; set; }
+ public string JEAEndpointName { get; set; } = "";
}
}
diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj
index 3e830324..02e61cb7 100644
--- a/IISU/WindowsCertStore.csproj
+++ b/IISU/WindowsCertStore.csproj
@@ -3,7 +3,7 @@
Keyfactor.Extensions.Orchestrator.WindowsCertStore
true
- net6.0;net8.0
+ net8.0;net10.0
AnyCPU
disable
@@ -31,30 +31,44 @@
-
-
-
-
-
-
+
+
+
+
-
-
+
PreserveNewest
-
+
Always
-
+
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index adb31708..f81e6025 100644
--- a/README.md
+++ b/README.md
@@ -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.
-
+
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.)
@@ -70,21 +72,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 +91,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:
@@ -102,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
@@ -113,40 +111,505 @@ 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:
+
+```text
+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: (Optional) Create the Audit Transcript Directory
+
+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
+```
+
+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`.
+
+---
+
+#### 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$'
+ ```
+
+ 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):**
+
+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'` |
+
+**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
+
+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 }
+
+# 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' }
+
+# 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.
+
+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
+
+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
+
+#### 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 **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..."**
+
+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 (if transcript logging is enabled)**
+
+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
+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.
+
+* 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:
@@ -159,12 +622,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
@@ -176,7 +639,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.
@@ -189,24 +651,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:
@@ -225,10 +685,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:
@@ -239,11 +699,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 |
@@ -252,18 +712,18 @@ the Keyfactor Command Portal
The Basic tab should look like this:
- 
+ 
##### 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:
- 
+ 
> 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.
@@ -278,34 +738,31 @@ 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:
- 
-
+ 
###### SPN With Port
Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### Server Username
@@ -316,8 +773,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)
@@ -326,16 +781,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)
- 
- 
+ 
+ 
+ ###### JEA End Point Name
+ Name of the JEA endpoint to use for the session (This field is automatically created)
+ 
+ 
##### Entry Parameters Tab
@@ -346,15 +803,12 @@ the Keyfactor Command Portal
The Entry Parameters tab should look like this:
- 
-
-
+ 
##### 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'
- 
- 
-
+ 
+ 
@@ -364,8 +818,9 @@ the Keyfactor Command Portal
Click to expand details
+#### Key Features and Representation
-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.
+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,24 +917,32 @@ 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.
#### 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:
@@ -498,10 +961,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:
@@ -512,11 +975,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 |
@@ -525,18 +988,18 @@ the Keyfactor Command Portal
The Basic tab should look like this:
- 
+ 
##### 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:
- 
+ 
> 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.
@@ -551,34 +1014,31 @@ 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:
- 
-
+ 
###### SPN With Port
Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### Server Username
@@ -589,8 +1049,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)
@@ -599,16 +1057,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)
- 
- 
+ 
+ 
+ ###### JEA End Point Name
+ Name of the JEA endpoint to use for the session (This field is automatically created)
+ 
+ 
##### Entry Parameters Tab
@@ -625,57 +1085,54 @@ the Keyfactor Command Portal
The Entry Parameters tab should look like this:
- 
-
-
+ 
##### Port
String value specifying the IP port to bind the certificate to for the IIS site. Example: '443' for HTTPS.
- 
- 
+ 
+ 
##### 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.
- 
- 
+ 
+ 
##### 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'.
- 
- 
+ 
+ 
##### 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'.
- 
- 
+ 
+ 
##### 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.)
- 
- 
+ 
+ 
##### Protocol
Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication.
- 
- 
+ 
+ 
##### 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'
- 
- 
-
+ 
+ 
@@ -685,7 +1142,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
@@ -694,24 +1150,69 @@ 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 |
-|--------------|------------------------------------------------------------------------------------------------------------------------|
-| Add | ✅ Checked |
-| Remove | ✅ Checked |
-| Discovery | 🔲 Unchecked |
-| Reenrollment | 🔲 Unchecked |
-| Create | 🔲 Unchecked |
+| Operation | Is Supported |
+|--------------|--------------|
+| Add | ✅ Checked |
+| Remove | ✅ Checked |
+| Discovery | 🔲 Unchecked |
+| Reenrollment | ✅ Checked |
+| 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:
@@ -730,10 +1231,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:
@@ -744,11 +1245,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 | ✅ 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 |
| Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
@@ -757,18 +1258,18 @@ the Keyfactor Command Portal
The Basic tab should look like this:
- 
+ 
##### 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:
- 
+ 
> 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.
@@ -784,34 +1285,31 @@ 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:
- 
-
+ 
###### SPN With Port
Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### Server Username
@@ -822,8 +1320,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)
@@ -832,24 +1328,25 @@ 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)
- 
- 
-
+ 
+ 
###### 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.
- 
- 
+ 
+ 
+ ###### JEA End Point Name
+ Name of the JEA endpoint to use for the session (This field is automatically created)
+ 
+ 
##### Entry Parameters Tab
@@ -861,22 +1358,19 @@ the Keyfactor Command Portal
The Entry Parameters tab should look like this:
- 
-
-
+ 
##### 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.
- 
- 
+ 
+ 
##### 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'
- 
- 
-
+ 
+ 
@@ -886,35 +1380,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:
@@ -933,10 +1422,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:
@@ -947,11 +1436,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 |
@@ -960,18 +1449,18 @@ the Keyfactor Command Portal
The Basic tab should look like this:
- 
+ 
##### 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:
- 
+ 
> 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.
@@ -989,31 +1478,27 @@ the Keyfactor Command Portal
The Custom Fields tab should look like this:
- 
-
+ 
###### SPN With Port
Internally set the -IncludePortInSPN option when creating the remote PowerShell connection. Needed for some Kerberos configurations.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### 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.
- 
- 
-
+ 
+ 
###### Server Username
@@ -1024,8 +1509,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)
@@ -1034,16 +1517,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)
- 
- 
-
-
-
+ 
+ 
##### Entry Parameters Tab
@@ -1054,15 +1532,12 @@ the Keyfactor Command Portal
The Entry Parameters tab should look like this:
- 
-
-
+ 
##### 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'
- 
- 
-
+ 
+ 
@@ -1073,18 +1548,17 @@ 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/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` | `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` |
- | 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` | `LatestMajor` | `net8.0` |
+ | Between `11.6.0` and `24.x` | `net8.0` | | `net8.0` |
+ | `25.0` _and_ newer | `net10.0` | | `net10.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 `net10.0`.
2. **Locate the Universal Orchestrator extensions directory.**
@@ -1102,25 +1576,20 @@ the Keyfactor Command Portal
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
@@ -1135,8 +1604,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). |
@@ -1148,11 +1617,10 @@ 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) |
-
-
#### Using kfutil CLI
Click to expand details
@@ -1179,6 +1647,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**
@@ -1188,7 +1657,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
@@ -1204,15 +1672,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
@@ -1227,8 +1692,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). |
@@ -1240,11 +1705,10 @@ 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) |
-
-
#### Using kfutil CLI
Click to expand details
@@ -1271,6 +1735,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**
@@ -1280,7 +1745,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
@@ -1296,15 +1760,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
@@ -1319,8 +1780,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). |
@@ -1333,11 +1794,10 @@ 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) |
-
-
#### Using kfutil CLI
Click to expand details
@@ -1365,6 +1825,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**
@@ -1374,7 +1835,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
@@ -1390,17 +1850,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
@@ -1415,8 +1872,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. |
@@ -1431,8 +1888,6 @@ When creating a Certificate Store for WinADFS, the Client Machine name must be s
-
-
#### Using kfutil CLI
Click to expand details
@@ -1468,7 +1923,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
@@ -1484,24 +1938,21 @@ 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/WindowsCertStore.IntegrationTests/ClientConnection.cs b/WindowsCertStore.IntegrationTests/ClientConnection.cs
index 4099ad3b..50eae5e4 100644
--- a/WindowsCertStore.IntegrationTests/ClientConnection.cs
+++ b/WindowsCertStore.IntegrationTests/ClientConnection.cs
@@ -8,8 +8,10 @@ namespace WindowsCertStore.IntegrationTests
{
public class ClientConnection
{
- public string Machine { get; set; }
- public string Username { get; set; }
- public string PrivateKey { get; set; } // SSH private key
+ 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; }
}
}
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/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..5309a52c
--- /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..3e9ba81c 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()
@@ -19,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,
@@ -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..a96e9214 100644
--- a/WindowsCertStore.IntegrationTests/servers.json
+++ b/WindowsCertStore.IntegrationTests/servers.json
@@ -1,3 +1,8 @@
[
- { "Machine": "{IPAddress}" }
+ { "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": "192.168.230.139", "StoreType": "WinCert", "JEAEndpointName": "" },
+ { "Machine": "192.168.230.139", "StoreType": "WinCert", "JEAEndpointName": "keyfactor.wincert" }
]
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/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..ec07f0d8
--- /dev/null
+++ b/WindowsCertStore.UnitTests/PSHelperConfigTests.cs
@@ -0,0 +1,67 @@
+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("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}");
+ }
+
+ [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}");
+ }
+ }
+}
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/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;
+ }
+ }
+}
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]
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
+
diff --git a/docsource/content.md b/docsource/content.md
index 87a03395..506ccc4d 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.
-
+
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
@@ -58,40 +62,505 @@ 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:
+
+```text
+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: (Optional) Create the Audit Transcript Directory
+
+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
+```
+
+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`.
+
+---
+
+#### 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$'
+ ```
+
+ 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):**
+
+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'` |
+
+**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
+
+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 }
+
+# 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' }
+
+# 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.
+
+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
+
+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
+
+#### 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 **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..."**
+
+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 (if transcript logging is enabled)**
+
+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
+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.
+
+* 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:
@@ -104,13 +573,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.
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
+
+
+
+
+
+
+
+
+ Freeform
+
+ Fixed
+
+ Multiple Choice
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ Name
+
+ IIS Bound Certificate
+ Short Name
+
+ IISU
+ Custom Capability
+
+
+ Custom Capability
+
+
+
+
+
+
+
+ Inventory
+
+
+ Add
+
+
+ Remove
+
+ Create
+
+ Discovery
+
+
+ ODKG
+
+
+
+
+
+
+
+ Needs Server
+
+ Blueprint Allowed
+
+ Uses PowerShell
+
+
+
+
+
+
+ Requires Store Password
+
+ Supports Entry Password
+
\ No newline at end of file
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-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..286d9526
--- /dev/null
+++ b/docsource/images/IISU-custom-fields-store-type-dialog.svg
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ JEA End Point Name
+ String
+
\ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ Freeform
+
+ Fixed
+
+ Multiple Choice
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ Name
+
+ ADFS Rotation Manager
+ Short Name
+
+ WinAdfs
+ Custom Capability
+
+
+ Custom Capability
+
+
+
+
+
+
+
+ Inventory
+
+
+ Add
+
+ Remove
+
+ Create
+
+ Discovery
+
+ ODKG
+
+
+
+
+
+
+
+ Needs Server
+
+
+ Blueprint Allowed
+
+ Uses PowerShell
+
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ Freeform
+
+ Fixed
+
+ Multiple Choice
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ Name
+
+ Windows Certificate
+ Short Name
+
+ WinCert
+ Custom Capability
+
+
+ Custom Capability
+
+
+
+
+
+
+
+ Inventory
+
+
+ Add
+
+
+ Remove
+
+ Create
+
+ Discovery
+
+
+ ODKG
+
+
+
+
+
+
+
+ Needs Server
+
+ Blueprint Allowed
+
+ Uses PowerShell
+
+
+
+
+
+
+ Requires Store Password
+
+ Supports Entry Password
+
\ 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-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..286d9526
--- /dev/null
+++ b/docsource/images/WinCert-custom-fields-store-type-dialog.svg
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ JEA End Point Name
+ String
+
\ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ Freeform
+
+ Fixed
+
+ Multiple Choice
+
+
+
+
+
+
+ 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..6213662f
--- /dev/null
+++ b/docsource/images/WinSql-basic-store-type-dialog.svg
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ Edit Certificate Store Type
+
+
+
+ Basic
+
+ Advanced
+ Custom Fields
+ Entry Parameters
+
+
+
+
+
+
+ Name
+
+ WinSql
+ Short Name
+
+ WinSql
+ Custom Capability
+
+
+ Custom Capability
+
+
+
+
+
+
+
+ Inventory
+
+
+ Add
+
+
+ Remove
+
+ Create
+
+ Discovery
+
+
+ ODKG
+
+
+
+
+
+
+
+ Needs Server
+
+
+ Blueprint Allowed
+
+ Uses PowerShell
+
+
+
+
+
+
+ Requires Store Password
+
+ Supports Entry Password
+
\ No newline at end of file
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-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..c806c285
--- /dev/null
+++ b/docsource/images/WinSql-custom-fields-store-type-dialog.svg
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+ Edit Certificate Store Type
+
+
+
+ Basic
+ Advanced
+ Custom Fields
+
+ Entry Parameters
+
+
+
+
+
+ ADD
+
+ EDIT
+
+ DELETE
+ Total: 8
+
+
+ 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
+
+
+
+
+
+
+ JEA End Point Name
+ String
+
\ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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/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": [
diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh
index eaecbf43..db34988b 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,17 @@ 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)"
+ },
+ {
+ "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": [
@@ -145,14 +118,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 +145,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 +154,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 +163,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 +190,17 @@ 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)"
+ },
+ {
+ "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": [
@@ -315,14 +320,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",
@@ -331,7 +337,7 @@ create_store_type "WinSql" '{
"Add": true,
"Create": false,
"Discovery": false,
- "Enrollment": false,
+ "Enrollment": true,
"Remove": true
},
"Properties": [
@@ -341,7 +347,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 +356,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 +365,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 +392,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 +401,17 @@ 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."
+ },
+ {
+ "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": [
@@ -415,14 +453,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 +480,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 +489,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 +498,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 +525,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 +556,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..2809ae4d 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"
-}
-
-# ---------------------------------------------------------------------------
-# 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
-}
+# 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
-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)"
- }
+$Headers = @{
+ "Authorization" = "Bearer $KeyfactorAuthToken"
+ "Content-Type" = "application/json"
+ "x-keyfactor-requested-with" = "APIClient"
}
-# ---------------------------------------------------------------------------
-# 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,17 @@ 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)"
+ },
+ {
+ "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": [
@@ -138,15 +118,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 +145,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 +154,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 +163,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 +190,17 @@ 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)"
+ },
+ {
+ "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": [
@@ -310,15 +320,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",
@@ -328,7 +337,7 @@ New-StoreType "WinSql" @'
"Add": true,
"Create": false,
"Discovery": false,
- "Enrollment": false,
+ "Enrollment": true,
"Remove": true
},
"Properties": [
@@ -338,7 +347,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 +356,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 +365,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 +392,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 +401,17 @@ 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."
+ },
+ {
+ "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": [
@@ -412,15 +453,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 +480,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 +489,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 +498,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 +525,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 +556,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."