Scripts & Bits
Stuff that actually works.
Practical PowerShell for PKI administrators and Windows Server engineers. Tested in real environments, documented honestly, ready to adapt.
These are the kind of scripts you get from the smartest person in the office — the one who's already hit every edge case so you don't have to. Test in a lab first. You know the drill.
CA Health & Monitoring
Get-CAHealthSummary
CRL validity, CA cert expiry, and service state for all CAs in the domain
# Get-CAHealthSummary.ps1 # Polls every Enterprise CA in the current forest. # Reports CRL validity, CA cert expiry, and ADCS service state. # Requires: RSAT-ADCS tools on the machine running the script. function Get-CAHealthSummary { [CmdletBinding()] param ( $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() ) Import-Module ActiveDirectory -ErrorAction Stop # Discover all Enterprise CAs from AD $configNC = (Get-ADRootDSE).configurationNamingContext $caObjects = Get-ADObject -SearchBase "CN=Public Key Services,CN=Services,$configNC" ` -Filter {objectClass -eq "pKIEnrollmentService"} ` -Properties dNSHostName, cACertificate, cACertificateDN $results = foreach ($ca in $caObjects) { $hostname = $ca.dNSHostName $health = [PSCustomObject]@{ CAName = $ca.Name Host = $hostname CACertExpiry = $null CRLExpiry = $null ServiceState = "Unknown" Status = "Unknown" } try { # CA certificate expiry $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $ca.cACertificate[0]) $health.CACertExpiry = $cert.NotAfter $certDays = ($cert.NotAfter - [DateTime]::UtcNow).Days # CRL — read from CDP path $cdp = certutil -config "${hostname}\" -getreg CA\CRLPublicationURLs 2>&1 $crlPath = ($cdp | Select-String '^\s*1:').Line -replace '.*1:\s*' if ($crlPath -match '^C:\\') { $crl = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($crlPath) $crlDate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($crlPath) $health.CRLExpiry = certutil -CRL 2>&1 | Select-String 'Next CRL' | Select-Object -First 1 } # Service state via WMI $svc = Get-Service -ComputerName $hostname -Name 'CertSvc' -ErrorAction Stop $health.ServiceState = $svc.Status $health.Status = if ($certDays -lt 30) { "⚠ CERT EXPIRING" } elseif ($svc.Status -ne 'Running') { "⚠ SERVICE DOWN" } else { "OK" } } catch { $health.Status = "ERROR: $($_.Exception.Message)" } $health } $results | Sort-Object Status, CAName | Format-Table -AutoSize } Get-CAHealthSummary
Watch-CRLExpiry
Alert when any CRL in the enterprise drops below a threshold — pipe to email or Teams
# Watch-CRLExpiry.ps1 # Checks CRL validity for specified CAs. Returns objects for each CA # near or past expiry. Designed to pipe into Send-MailMessage or similar. # Schedule via Task Scheduler — daily is usually sufficient. [CmdletBinding()] param ( [string[]]$CAHosts = @('ca01.corp.local', 'ca02.corp.local'), [int] $WarnDays = 7, [int] $CriticalDays = 2 ) $alerts = foreach ($host in $CAHosts) { try { $crlInfo = certutil -config "${host}\" -CRL 2>&1 $nextLine = $crlInfo | Select-String 'Next CRL Publish' | Select-Object -First 1 if ($nextLine -match '(\d{1,2}/\d{1,2}/\d{4})') { $expiry = [DateTime]$Matches[1] $days = ($expiry - [DateTime]::Now).Days if ($days -le $WarnDays) { [PSCustomObject]@{ CAHost = $host CRLExpiry = $expiry DaysLeft = $days Severity = if ($days -le $CriticalDays) { 'CRITICAL' } else { 'WARNING' } } } } } catch { Write-Warning "Could not check ${host}: $($_.Exception.Message)" } } if ($alerts) { $alerts | Sort-Object DaysLeft | Format-Table -AutoSize # Pipe to alerting: $alerts | Send-MailMessage ... exit 1 # Non-zero exit for monitoring systems } Write-Host "All CRLs OK." -ForegroundColor Green
Certificate Operations
Get-CertsByTemplate
Find all issued certs from a specific template — with expiry and SANs
# Get-CertsByTemplate.ps1 # Queries the CA database for all issued certs from a given template. # Returns subject, SAN, expiry, requester, and thumbprint. # Requires local admin on the CA server (or Remote CA access). [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$TemplateName, # Short name (not OID or display name) [string]$CAConfig = '', # "server\CAName" — auto-detected if blank [int] $ExpiringInDays = 0, # 0 = all; >0 = only expiring within N days [switch]$IncludeRevoked # Also return revoked certificates ) if (-not $CAConfig) { $CAConfig = (certutil -getconfig 2>&1 | Select-String 'Config').Line -replace '.*"(.*)".*', '$1' Write-Verbose "Auto-detected CA: $CAConfig" } $filter = "CertificateTemplate == `"$TemplateName`"" $rows = certutil -config $CAConfig -view -restrict $filter ` -out "RequestID,RequesterName,CommonName,NotAfter,CertificateTemplate,StatusCode" 2>&1 # Parse certutil's tabular output $rows | Where-Object { $_ -match 'NotAfter' } | ForEach-Object { # (In production you'd parse the full block — this is a starting point) Write-Verbose $_ } # Or use DCOM (requires COM interop — more reliable for large DBs) $caView = New-Object -ComObject CertificateAuthority.View $caView.OpenConnection($CAConfig) $caView.SetRestriction(-1, 1, 0, $TemplateName) # Column -1 = template $resultSet = $caView.OpenView() $certs = while ($resultSet.Next() -ne -1) { [PSCustomObject]@{ ReqID = $resultSet.GetValue(0, 0) Subject = $resultSet.GetValue(1, 0) Requester = $resultSet.GetValue(2, 0) NotAfter = [DateTime]$resultSet.GetValue(3, 0) DaysLeft = ($resultSet.GetValue(3, 0) - [DateTime]::Now).Days } } $certs = if ($ExpiringInDays -gt 0) { $certs | Where-Object { $_.DaysLeft -le $ExpiringInDays -and $_.DaysLeft -ge 0 } } else { $certs } $certs | Sort-Object DaysLeft | Format-Table -AutoSize
Request-CertFromCA
PowerShell-native cert request and retrieval — no certreq.exe required
# Request-CertFromCA.ps1 # Requests a certificate using Get-Certificate (PS 5.1+). # Cleaner than certreq.exe and natively pipe-friendly. [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [string] $Template, # Certificate template short name [string] $SubjectName = "CN=$env:COMPUTERNAME", [string[]]$DnsNames = @($env:COMPUTERNAME), [string] $Store = 'LocalMachine\My', [string] $CAConfig = '' # "server\CAName" or blank for auto ) $enrollParams = @{ Template = $Template SubjectName = $SubjectName DnsName = $DnsNames CertStoreLocation = "Cert:\$Store" } if ($CAConfig) { $enrollParams['Url'] = "ldap:///$([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name)" } Write-Verbose "Requesting $Template cert for $SubjectName..." $result = Get-Certificate @enrollParams if ($result.Status -eq 'Issued') { $cert = $result.Certificate [PSCustomObject]@{ Status = 'Issued' Subject = $cert.Subject Thumbprint = $cert.Thumbprint NotAfter = $cert.NotAfter SerialNo = $cert.SerialNumber } } else { Write-Error "Certificate request returned status: $($result.Status)" }
Security & Assessment
Test-CAPermissions
Audit who has Manage CA, Issue and Manage, and Enroll permissions — and flag anything unexpected
# Test-CAPermissions.ps1 # Dumps and evaluates CA security permissions. # Flags anything beyond Domain Admins / CA Admins having elevated rights. # Run from a machine with RSAT-ADCS or directly on the CA. [CmdletBinding()] param ( [string]$CAHost = $env:COMPUTERNAME, [string]$CAName = '', [string[]]$AllowedAdmins = @('Domain Admins', 'CA Admins', 'Enterprise Admins') ) $config = if ($CAName) { "$CAHost\$CAName" } else { (certutil -config - -ping 2>&1 | Select-String '"(.*)"').Matches[0].Groups[1].Value } # Read the CA SD via certutil $sdOutput = certutil -config $config -getreg CA\Security 2>&1 # Parse permission lines — certutil format: " Account : Rights (hex)" $permPattern = [regex]'^\s+(\S.*?)\s*:\s+(.+)\(0x[\da-fA-F]+\)' $permissions = $sdOutput | Where-Object { $permPattern.IsMatch($_) } | ForEach-Object { $m = $permPattern.Match($_) [PSCustomObject]@{ Account = $m.Groups[1].Value.Trim() Rights = $m.Groups[2].Value.Trim() IsAdmin = $AllowedAdmins | Where-Object { $m.Groups[1].Value -like "*$_*" } } } # Flag any account with Manage CA that isn't in the allow list $unexpected = $permissions | Where-Object { $_.Rights -match 'Manage CA|Officer|Administrator' -and -not $_.IsAdmin } Write-Host "`nCA: $config" -ForegroundColor Cyan $permissions | Format-Table Account, Rights -AutoSize if ($unexpected) { Write-Warning "$($unexpected.Count) unexpected elevated permission(s) found:" $unexpected | Format-Table Account, Rights -AutoSize } else { Write-Host "No unexpected elevated permissions found." -ForegroundColor Green }
Utilities
Export-CertInventory
Full certificate inventory from all CAs to a single CSV — great for auditors who love spreadsheets
# Export-CertInventory.ps1 # Generates a CSV of all non-expired issued certs from specified CAs. # Output includes subject, SAN, template, requester, expiry, status. # Pipe to Out-GridView for quick interactive browsing. [CmdletBinding()] param ( [string[]]$CAConfigs = @(), # "server\caname" — auto-detect if empty [string] $OutputPath = ".\CertInventory_$(Get-Date -f yyyyMMdd).csv", [switch] $IncludeRevoked, [switch] $GridView ) if (-not $CAConfigs) { # Auto-discover all Enterprise CAs $configNC = (Get-ADRootDSE).configurationNamingContext $CAConfigs = (Get-ADObject -SearchBase "CN=Enrollment Services,CN=Public Key Services,CN=Services,$configNC" ` -Filter * -Properties dNSHostName, Name).ForEach({ "$($_.dNSHostName)\$($_.Name)" }) } $all = foreach ($cfg in $CAConfigs) { Write-Progress -Activity "Querying" -Status $cfg certutil -config $cfg -view -restrict "Disposition=20" # 20 = Issued ` -out "RequestID,RequesterName,CommonName,NotAfter,CertificateTemplate,SerialNumber" 2>&1 | Where-Object { $_ -match '^[^:]+:\s+\S' } | ForEach-Object { "$cfg,$_" } } if ($GridView) { $all | ConvertFrom-Csv -Header CA,Field,Value | Out-GridView -Title "Certificate Inventory" } else { $all | Set-Content $OutputPath -Encoding UTF8 Write-Host "Exported to $OutputPath" -ForegroundColor Green }
Have a script to contribute, a bug to report, or a scenario these don't cover?
Send it over. If it's useful enough to share, it'll end up here.