Skip to content

Commit 8405f5e

Browse files
author
Bob Pokorny
committed
Updated PowerShell initialization to check for appropriate permissions when running IIS jobs.
1 parent 3825af5 commit 8405f5e

10 files changed

Lines changed: 92 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* .NET6 assemblies are no longer supported.
66
* Fixed a problem when passing a bad CSP, the job was reporting successful, but did not actually add/bind the certificate.
77
* 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.).
8+
* Since adding JEA, lower privileged users can now run the extension in a local PowerShell Runspace without needing to be a member of the Administrators group. This is a security improvement, but it does require that the user has been granted access to the JEA endpoint on the target server. When running IIS jobs without JEA endpoints, the credentials entered will still need Administrative permissions. If you are running into permission issues, please check your JEA configuration and ensure that the user has been granted access to the endpoint.
9+
* Fixed IIS Inventory error propagation across all connection types (Local, WinRM, JEA, SSH). Previously, when the cmdlet encountered errors (e.g., insufficient admin rights), it used Write-Warning/Write-Information which didn't signal failure to the orchestrator. Over remote sessions, PS.HadErrors remained false and jobs returned 0 certificates with no indication of failure, making it impossible to distinguish between "no certificates found" vs "access denied". The cmdlet now returns structured KeyfactorResult JSON objects for all error scenarios, which the orchestrator detects and reports in JobResult.FailureMessage. This ensures reliable error detection and reporting regardless of connection type (Local PowerShell, WinRM HTTP/HTTPS, JEA endpoints, SSH sessions).
810

911
3.0.2
1012

@@ -23,26 +25,27 @@
2325
* Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags
2426
* Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags
2527
* Added SSL Flag validation to make sure the bit flag is correct. These are the valid bit flags for the version of Windows:
26-
### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)
28+
### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5)
29+
2730

2831
* 0 No SNI
2932
* 1 Use SNI
3033
* 2 Use Centralized SSL certificate store.
3134

32-
### Windows Server 2016 (IIS 10.0)
35+
### Windows Server 2016 (IIS 10.0)
3336

3437
* 0 No SNI
3538
* 1 Use SNI
3639
* 4 Disable HTTP/2.
3740

38-
### Windows Server 2019 (IIS 10.0.17763)
41+
### Windows Server 2019 (IIS 10.0.17763)
3942

4043
* 0 No SNI
4144
* 1 Use SNI
4245
* 4 Disable HTTP/2.
4346
* 8 Disable OCSP Stapling.
4447

45-
### Windows Server 2022+ (IIS 10.0.20348+)
48+
### Windows Server 2022+ (IIS 10.0.20348+)
4649

4750
* 0 No SNI
4851
* 1 Use SNI

IISU/ClientPSCertStoreReEnrollment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit
9494
string clientMachineName = config.CertificateStoreDetails.ClientMachine;
9595
string storePath = config.CertificateStoreDetails.StorePath;
9696

97-
_psHelper = new(protocol, port, includePortInSPN, clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint);
97+
_psHelper = new(protocol, port, includePortInSPN, clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint, adminPrivilegesRequired: bindingType == CertStoreBindingTypeENUM.WinIIS);
9898
_psHelper.Initialize();
9999

100100
using (_psHelper)

IISU/ImplementedStoreTypes/WinIIS/Inventory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public List<CurrentInventoryItem> QueryIISCertificates(RemoteSettings settings)
128128
{
129129
List<CurrentInventoryItem> Inventory = new();
130130

131-
using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName))
131+
using (PSHelper ps = new(settings.Protocol, settings.Port, settings.IncludePortInSPN, settings.ClientMachineName, settings.ServerUserName, settings.ServerPassword, jeaEndpoint: settings.JEAEndpointName, adminPrivilegesRequired: true))
132132
{
133133
ps.Initialize();
134134

IISU/ImplementedStoreTypes/WinIIS/Management.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
9797
// Assign the binding information
9898
IISBindingInfo bindingInfo = new IISBindingInfo(config.JobProperties);
9999

100-
_psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint);
100+
_psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword, jeaEndpoint: jeaEndpoint, adminPrivilegesRequired:true);
101101

102102
_psHelper.Initialize();
103103

IISU/PSHelper.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class PSHelper : IDisposable
6262
private bool isADFSStore = false;
6363
private string jeaEndpoint = "";
6464
private bool useJea => !string.IsNullOrEmpty(jeaEndpoint);
65+
private bool adminPrivilegesRequired = false;
6566

6667
public bool IsLocalMachine
6768
{
@@ -97,7 +98,7 @@ public PSHelper()
9798
_logger = LogHandler.GetClassLogger<PSHelper>();
9899
}
99100

100-
public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false, string jeaEndpoint = "")
101+
public PSHelper(string protocol, string port, bool useSPN, string clientMachineName, string serverUserName, string serverPassword, bool isADFSStore = false, string jeaEndpoint = "", bool adminPrivilegesRequired = false)
101102
{
102103
this.protocol = protocol.ToLower();
103104
this.port = port;
@@ -107,6 +108,7 @@ public PSHelper(string protocol, string port, bool useSPN, string clientMachineN
107108
this.serverPassword = serverPassword;
108109
this.isADFSStore = isADFSStore;
109110
this.jeaEndpoint = jeaEndpoint;
111+
this.adminPrivilegesRequired = adminPrivilegesRequired;
110112

111113
_logger = LogHandler.GetClassLogger<PSHelper>();
112114
_logger.LogTrace("Entered PSHelper Constructor");
@@ -185,6 +187,57 @@ public void Initialize()
185187
_logger.LogTrace($"{result}");
186188
}
187189
}
190+
191+
// Check if admin privileges are required and if the current user has them
192+
if (adminPrivilegesRequired)
193+
{
194+
bool isAdmin;
195+
string errorMessage = null;
196+
197+
if (isLocalMachine)
198+
{
199+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
200+
{
201+
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
202+
var principal = new System.Security.Principal.WindowsPrincipal(identity);
203+
isAdmin = principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
204+
205+
if (!isAdmin)
206+
{
207+
errorMessage = $"Administrator privileges are required to perform this operation. " +
208+
$"Current identity: {identity.Name}.";
209+
}
210+
}
211+
else
212+
{
213+
isAdmin = false;
214+
errorMessage = "Administrator privileges are required to perform this operation.";
215+
}
216+
}
217+
else
218+
{
219+
// Single source of truth: call the same cmdlet PowerShell callers use
220+
const string checkAdminScript = "Test-KeyfactorAdminRights -Step 'RemoteAdminCheck'";
221+
var results = ExecutePowerShell(checkAdminScript, isScript: true);
222+
223+
if (results == null || results.Count == 0)
224+
{
225+
// Cmdlet returns $null when the identity has admin rights
226+
isAdmin = true;
227+
}
228+
else
229+
{
230+
isAdmin = false;
231+
errorMessage = results[0].Properties["ErrorMessage"]?.Value?.ToString()
232+
?? "Administrator privileges are required to perform this operation.";
233+
}
234+
}
235+
236+
if (!isAdmin)
237+
{
238+
throw new Exception(errorMessage);
239+
}
240+
}
188241
}
189242

190243
private void InitializeRemoteSession()

IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-KeyfactorAdminRights.ps1

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,37 @@ function Test-KeyfactorAdminRights {
22
[CmdletBinding()]
33
[OutputType([pscustomobject])]
44
param(
5-
[string]$Step = "AdminCheck"
5+
[string]$Step = "AdminCheck",
6+
[switch]$Force
67
)
78

8-
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
9-
$principal = [Security.Principal.WindowsPrincipal]::new($currentIdentity)
10-
11-
if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
12-
return $null
9+
# Return cached result if we've already checked this session (identity won't
10+
# change mid-session, so this avoids redundant token lookups on repeat calls)
11+
if (-not $Force -and $script:__KfAdminCheckDone) {
12+
return $script:__KfAdminCheckResult
1313
}
1414

15-
$errorMessage = "This operation requires an elevated session (Run as Administrator) " +
16-
"or a JEA endpoint whose RunAs identity has local Administrator rights on this machine. " +
17-
"Current identity: $($currentIdentity.Name)."
15+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
16+
try {
17+
$isAdmin = [Security.Principal.WindowsPrincipal]::new($identity).IsInRole(
18+
[Security.Principal.WindowsBuiltInRole]::Administrator)
1819

19-
Write-Warning $errorMessage
20+
if ($isAdmin) {
21+
$result = $null
22+
}
23+
else {
24+
$errorMessage = "This operation requires an elevated session (Run as Administrator) " +
25+
"or a JEA endpoint whose RunAs identity has local Administrator rights on this machine. " +
26+
"Current identity: $($identity.Name)."
27+
Write-Warning $errorMessage
28+
$result = New-KeyfactorResult -Status Error -Code 240 -Step $Step -ErrorMessage $errorMessage
29+
}
2030

21-
return New-KeyfactorResult -Status Error -Code 240 -Step $Step -ErrorMessage $errorMessage
22-
}
31+
$script:__KfAdminCheckResult = $result
32+
$script:__KfAdminCheckDone = $true
33+
return $result
34+
}
35+
finally {
36+
$identity.Dispose()
37+
}
38+
}

IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ function Get-KeyfactorIISBoundCertificates {
33
$certificates = @()
44
$totalBoundCertificates = 0
55

6-
#
7-
# Verify the current process is running with an elevated token.
8-
#
9-
$adminCheck = Test-KeyfactorAdminRights
10-
if ($adminCheck) {
11-
Write-Error $adminCheck.ErrorMessage -ErrorAction Stop
12-
}
13-
146
try {
157
Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" -ErrorAction Stop
168
$serverManager = [Microsoft.Web.Administration.ServerManager]::new()

IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525
Write-Information "[VERBOSE] Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')"
2626

2727
try {
28-
$adminCheck = Test-KeyfactorAdminRights
29-
if ($adminCheck) {
30-
return $adminCheck
31-
}
3228

3329
# Step 1: Perform verifications and get management info
3430
# Check SslFlags

IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ function Remove-KeyfactorIISCertificateIfUnused {
99
[string]$StoreName = "My"
1010
)
1111

12-
$adminCheck = Test-KeyfactorAdminRights
13-
if ($adminCheck) {
14-
return $adminCheck
15-
}
16-
1712
try {
1813
Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll"
1914
}

IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313
[System.Nullable[bool]]$UseIISDrive = $null
1414
)
1515

16-
$adminCheck = Test-KeyfactorAdminRights
17-
if ($adminCheck) {
18-
return $adminCheck
19-
}
20-
2116
# Auto-detect IIS Drive availability if not explicitly provided
2217
if ($null -eq $UseIISDrive) {
2318
$UseIISDrive = Test-IISDrive

0 commit comments

Comments
 (0)