Skip to content

Commit 3825af5

Browse files
author
Bob Pokorny
committed
Changes to be committed:
modified: IISU/ImplementedStoreTypes/WinIIS/Management.cs modified: IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1 new file: IISU/PowerShell/Keyfactor.WinCert.Common/Private/Test-KeyfactorAdminRights.ps1 modified: IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1 modified: IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc modified: IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Get-KeyfactorIISBoundCertificates.ps1 modified: IISU/PowerShell/Keyfactor.WinCert.IIS/Public/New-KeyfactorIISSiteBinding.ps1 modified: IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISCertificateIfUnused.ps1 modified: IISU/PowerShell/Keyfactor.WinCert.IIS/Public/Remove-KeyfactorIISSiteBinding.ps1
1 parent 226dcb3 commit 3825af5

9 files changed

Lines changed: 133 additions & 23 deletions

File tree

IISU/ImplementedStoreTypes/WinIIS/Management.cs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,14 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
205205
if (psResult == OrchestratorJobStatusJobResult.Success && !string.IsNullOrEmpty(alias))
206206
{
207207
_logger.LogTrace("Attempting to remove original certificate from store if it is no longer bound to any site.");
208-
RemoveIISCertificate(alias);
208+
var cleanupResult = RemoveIISCertificate(alias);
209+
if (cleanupResult != null)
210+
{
211+
// Binding already succeeded — a cleanup failure downgrades to a
212+
// Warning rather than masking the successful bind as a Failure.
213+
psResult = cleanupResult.Result;
214+
failureMessage = cleanupResult.FailureMessage;
215+
}
209216
_logger.LogTrace("Returned from removing cert if not used.");
210217
}
211218

@@ -250,10 +257,12 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
250257
{
251258
if (WinIISBinding.UnBindCertificate(_psHelper, thisBinding))
252259
{
253-
// This function will only remove the certificate from the store if not used by any other sites
254-
RemoveIISCertificate(thisBinding.Thumbprint);
260+
// This function will only remove the certificate from the store if not used by any other sites.
261+
// The unbind already succeeded — a cleanup failure downgrades to a Warning
262+
// rather than masking the successful unbind as a Failure.
263+
var cleanupResult = RemoveIISCertificate(thisBinding.Thumbprint);
255264

256-
complete = new JobResult
265+
complete = cleanupResult ?? new JobResult
257266
{
258267
Result = OrchestratorJobStatusJobResult.Success,
259268
JobHistoryId = _jobHistoryID,
@@ -338,7 +347,14 @@ public ResultObject AddCertificate(string certificateContents, string privateKey
338347
throw new Exception (niceMessage);
339348
}
340349
}
341-
public void RemoveIISCertificate(string thumbprint)
350+
/// <summary>
351+
/// Attempts to remove a certificate from the store if it is no longer bound to any IIS site.
352+
/// Returns null when there is nothing to report (removed, skipped because still in use, or not
353+
/// found). Returns a Warning JobResult when the cleanup itself failed, since at the point this is
354+
/// called the primary bind/unbind operation has already succeeded and a cleanup failure should
355+
/// not be reported to the orchestrator as if the whole job failed.
356+
/// </summary>
357+
public JobResult RemoveIISCertificate(string thumbprint)
342358
{
343359
_logger.LogTrace($"Attempting to remove thumbprint {thumbprint} from store {_storePath}");
344360

@@ -348,8 +364,41 @@ public void RemoveIISCertificate(string thumbprint)
348364
{ "StoreName", _storePath }
349365
};
350366

351-
_psHelper.ExecutePowerShell("Remove-KeyfactorIISCertificateIfUnused", parameters);
367+
try
368+
{
369+
var results = _psHelper.ExecutePowerShell("Remove-KeyfactorIISCertificateIfUnused", parameters);
370+
ResultObject result = ResultObject.FromPSResults(results);
371+
372+
_logger.LogTrace($"Remove-KeyfactorIISCertificateIfUnused returned Status={result.Status}, Code={result.Code}, Step={result.Step}");
352373

374+
if (string.Equals(result.Status, ResultObject.StatusError, StringComparison.OrdinalIgnoreCase))
375+
{
376+
var warningMessage = $"Certificate '{thumbprint}' could not be removed from store '{_storePath}': {result.ErrorMessage}";
377+
_logger.LogWarning(warningMessage);
378+
379+
return new JobResult
380+
{
381+
Result = OrchestratorJobStatusJobResult.Warning,
382+
JobHistoryId = _jobHistoryID,
383+
FailureMessage = warningMessage
384+
};
385+
}
386+
387+
// Success or Skipped (still in use elsewhere / not found) are both benign outcomes.
388+
return null;
389+
}
390+
catch (Exception ex)
391+
{
392+
var warningMessage = $"Certificate '{thumbprint}' could not be removed from store '{_storePath}': {ex.Message}";
393+
_logger.LogWarning(warningMessage);
394+
395+
return new JobResult
396+
{
397+
Result = OrchestratorJobStatusJobResult.Warning,
398+
JobHistoryId = _jobHistoryID,
399+
FailureMessage = warningMessage
400+
};
401+
}
353402
}
354403

355404
public JobResult RemoveCertificateORIG(string thumbprint)

IISU/PowerShell/Keyfactor.WinCert.Common/Keyfactor.WinCert.Common.psm1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
. "$PSScriptRoot\Private\Test-CryptoServiceProvider.ps1"
1212
. "$PSScriptRoot\Private\Validate-CryptoProvider.ps1"
1313
. "$PSScriptRoot\Private\Convert-DNSSubject.ps1"
14+
. "$PSScriptRoot\Private\Test-KeyfactorAdminRights.ps1"
1415

1516
# Public functions
1617
. "$PSScriptRoot\Public\New-KeyfactorResult.ps1"
@@ -33,7 +34,8 @@ Export-ModuleMember -Function @(
3334
'New-KeyfactorODKGEnrollment',
3435
'Import-KeyfactorSignedCertificate',
3536
'Get-KeyfactorDiagnostics',
36-
# Shared certificate inspection utilities — exported so other modules (e.g. IIS) can call them
37+
# Shared utilities — exported so other modules (e.g. IIS) can call them
3738
'Get-CertificateCSP',
38-
'Get-CertificateSAN'
39+
'Get-CertificateSAN',
40+
'Test-KeyfactorAdminRights'
3941
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
function Test-KeyfactorAdminRights {
2+
[CmdletBinding()]
3+
[OutputType([pscustomobject])]
4+
param(
5+
[string]$Step = "AdminCheck"
6+
)
7+
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
13+
}
14+
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)."
18+
19+
Write-Warning $errorMessage
20+
21+
return New-KeyfactorResult -Status Error -Code 240 -Step $Step -ErrorMessage $errorMessage
22+
}

IISU/PowerShell/Keyfactor.WinCert.Common/Public/New-KeyfactorResult.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# CompareThumbprint Checking if binding needs to be updated
1010
# BindSSL Adding SSL cert to a binding
1111
# ImportModules Importing IIS-related PowerShell modules
12+
# AdminCheck Verifying the caller has local Administrator rights
13+
# RemoveCertificate Checking/removing a certificate if unused by any binding
1214
# CatchAll Fallback for unexpected or generic errors
1315

1416
# Standard Error Codes
@@ -24,6 +26,11 @@
2426
# 205 Error Thumbprint mismatch
2527
# 206 Error WebAdministration module missing
2628
# 207 Error IISAdministration module missing
29+
# 208 Skipped Certificate still in use on another binding — removal skipped
30+
# 209 Skipped Certificate not found in store — nothing to remove
31+
# 231 Error Failed to load Microsoft.Web.Administration assembly
32+
# 232 Error Unexpected error while removing certificate from store
33+
# 240 Error Caller lacks local Administrator rights (shared check, Common module)
2734
# 300 Error Unknown or unhandled exception
2835
# 400 Error Invalid Ssl Flag bit combination
2936

IISU/PowerShell/Keyfactor.WinCert.Common/RoleCapabilities/Keyfactor.WinCert.Common.psrc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
ModulesToImport = @('Keyfactor.WinCert.Common')
3030

3131
# Functions the orchestrator is allowed to invoke in this JEA session.
32-
# Get-CertificateCSP and Get-CertificateSAN are also listed because other modules
33-
# (e.g. Keyfactor.WinCert.IIS) call them cross-module, which requires them to be exported.
32+
# Get-CertificateCSP, Get-CertificateSAN, and Test-KeyfactorAdminRights are also listed
33+
# because other modules (e.g. Keyfactor.WinCert.IIS) call them cross-module, which
34+
# requires them to be exported.
3435
VisibleFunctions = @(
3536
'Get-KeyfactorCertificates',
3637
'Get-KeyfactorDiagnostics',
@@ -40,7 +41,8 @@
4041
'New-KeyfactorODKGEnrollment',
4142
'Import-KeyfactorSignedCertificate',
4243
'Get-CertificateCSP',
43-
'Get-CertificateSAN'
44+
'Get-CertificateSAN',
45+
'Test-KeyfactorAdminRights'
4446
)
4547

4648
# Cmdlets available to caller scriptblocks (param() wrappers sent by PSHelper).

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@ function Get-KeyfactorIISBoundCertificates {
66
#
77
# Verify the current process is running with an elevated token.
88
#
9-
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
10-
$principal = [Security.Principal.WindowsPrincipal]::new($currentIdentity)
11-
12-
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
13-
Write-Information `
14-
-Message "Get-KeyfactorIISBoundCertificates requires an elevated PowerShell session (Run as Administrator) or a JEA endpoint configured with administrative privileges." `
15-
-ErrorAction Stop
9+
$adminCheck = Test-KeyfactorAdminRights
10+
if ($adminCheck) {
11+
Write-Error $adminCheck.ErrorMessage -ErrorAction Stop
1612
}
1713

1814
try {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
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+
}
32+
2833
# Step 1: Perform verifications and get management info
2934
# Check SslFlags
3035
$sslValidationResult = Test-ValidSslFlags -Flags $SslFlags

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
function Remove-KeyfactorIISCertificateIfUnused {
2+
[CmdletBinding()]
3+
[OutputType([pscustomobject])]
24
param (
35
[Parameter(Mandatory = $true)]
46
[string]$Thumbprint,
@@ -7,10 +9,18 @@ function Remove-KeyfactorIISCertificateIfUnused {
79
[string]$StoreName = "My"
810
)
911

12+
$adminCheck = Test-KeyfactorAdminRights
13+
if ($adminCheck) {
14+
return $adminCheck
15+
}
16+
1017
try {
1118
Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll"
12-
} catch {
13-
throw "Failed to load Microsoft.Web.Administration. Ensure IIS is installed on the remote server."
19+
}
20+
catch {
21+
$errorMessage = "Failed to load Microsoft.Web.Administration. Ensure IIS is installed on the remote server."
22+
Write-Warning $errorMessage
23+
return New-KeyfactorResult -Status Error -Code 231 -Step RemoveCertificate -ErrorMessage $errorMessage
1424
}
1525

1626
# Normalize thumbprint: strip whitespace and force uppercase for consistent comparison
@@ -36,23 +46,35 @@ function Remove-KeyfactorIISCertificateIfUnused {
3646
}
3747

3848
if ($bindings.Count -gt 0) {
49+
$bindingSummary = ($bindings | ForEach-Object { "$($_.SiteName) / $($_.Binding)" }) -join ", "
3950
Write-Information "[VERBOSE] Certificate $normalizedThumbprint is still active on $($bindings.Count) binding(s) — skipping removal"
4051
$bindings | ForEach-Object { Write-Warning " Still bound: $($_.SiteName) / $($_.Binding)" }
41-
return
52+
53+
return New-KeyfactorResult -Status Skipped -Code 208 -Step RemoveCertificate `
54+
-Message "Certificate $normalizedThumbprint is still bound to $($bindings.Count) site(s) ($bindingSummary); removal skipped." `
55+
-Details @{ Thumbprint = $normalizedThumbprint; StillBoundTo = $bindings }
4256
}
4357

4458
$cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" |
4559
Where-Object { $_.Thumbprint -eq $normalizedThumbprint }
4660

4761
if (-not $cert) {
4862
Write-Information "[VERBOSE] Certificate $normalizedThumbprint not found in Cert:\LocalMachine\$StoreName — nothing to remove"
49-
return
63+
return New-KeyfactorResult -Status Skipped -Code 209 -Step RemoveCertificate `
64+
-Message "Certificate $normalizedThumbprint was not found in Cert:\LocalMachine\$StoreName; nothing to remove." `
65+
-Details @{ Thumbprint = $normalizedThumbprint }
5066
}
5167

5268
Remove-Item -Path "Cert:\LocalMachine\$StoreName\$normalizedThumbprint" -Force
5369
Write-Information "[VERBOSE] Certificate $normalizedThumbprint removed from Cert:\LocalMachine\$StoreName"
70+
71+
return New-KeyfactorResult -Status Success -Code 0 -Step RemoveCertificate `
72+
-Message "Certificate $normalizedThumbprint removed from store '$StoreName'." `
73+
-Details @{ Thumbprint = $normalizedThumbprint }
5474
}
5575
catch {
56-
Write-Error "An error occurred while attempting to remove IIS certificate: $_"
76+
$errorMessage = "An error occurred while attempting to remove IIS certificate: $_"
77+
Write-Warning $errorMessage
78+
return New-KeyfactorResult -Status Error -Code 232 -Step RemoveCertificate -ErrorMessage $errorMessage -Details @{ Thumbprint = $normalizedThumbprint }
5779
}
5880
}

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

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

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

0 commit comments

Comments
 (0)