19. September 2026 • 6 min. read

Signing PowerShell Scripts with Your Own Code Signing Certificate

windowspowershell

Hello everyone,

a couple of days ago, I was trying to find a specific keyword in all the GPOs we have. For this I used a simple PowerShell script that searches through every GPO and outputs the GPO that contains the keyword.

Simple, but it works.

$results = foreach ($gpo in Get-GPO -All) {
    $xml = Get-GPOReport -Guid $gpo.Id -ReportType Xml
    if ($xml -match 'your-keyword') {
        [PSCustomObject]@{
            Name = $gpo.DisplayName
            Id   = $gpo.Id
        }
    }
}
$results

The output looks like this.

Name                         Id
----                         --
TZ_Security_IE_without_Proxy 252f7934-ee58-4824-a6cd-124f241b2c3d

Since I did not want to input this myself every time I want to search for something, I let Claude create a PowerShell script around this.

Here is the script, if you are interested. It works and gives a clean output.

<#
.SYNOPSIS
    Searches all GPOs in the domain for one or more keywords.

.DESCRIPTION
    Pulls the XML report for every GPO via Get-GPOReport and searches it
    for the given keyword(s). Case-insensitive by default. No files are
    written to disk unless -ExportPath is specified.

.PARAMETER Keyword
    One or more keywords/strings to search for. Accepts an array.

.PARAMETER Regex
    If specified, treats the Keyword value(s) as regex patterns instead
    of plain text.

.PARAMETER CaseSensitive
    If specified, performs a case-sensitive match.

.PARAMETER ExportPath
    Optional folder. If provided, also saves each GPO's XML report there
    (useful if you want to keep a local copy or search repeatedly with
    other tools like grep/ripgrep).

.PARAMETER ShowContext
    If specified, prints the matching line(s) from each hit, not just
    the GPO name.

.EXAMPLE
    .\Search-GPOKeyword.ps1 -Keyword "Your-Keyword"

.EXAMPLE
    .\Search-GPOKeyword.ps1 -Keyword "Your-Keyword","VPN","logon.bat" -ShowContext

.EXAMPLE
    .\Search-GPOKeyword.ps1 -Keyword "H:\\.*Your-Keyword" -Regex -ShowContext

.EXAMPLE
    .\Search-GPOKeyword.ps1 -Keyword "Your-Keyword" -ExportPath "C:\GPOReports"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string[]]$Keyword,

    [switch]$Regex,

    [switch]$CaseSensitive,

    [string]$ExportPath,

    [switch]$ShowContext
)

Import-Module GroupPolicy -ErrorAction Stop

if ($ExportPath -and -not (Test-Path $ExportPath)) {
    New-Item -ItemType Directory -Path $ExportPath -Force | Out-Null
}

# Build the patterns. If -Regex wasn't specified, escape the keywords so
# they're treated as literal text rather than regex syntax.
$patterns = foreach ($k in $Keyword) {
    if ($Regex) { $k } else { [regex]::Escape($k) }
}

$allGpos = Get-GPO -All
Write-Host "Searching $($allGpos.Count) GPOs for: $($Keyword -join ', ')" -ForegroundColor Cyan
if ($CaseSensitive) { Write-Host "(case-sensitive)" -ForegroundColor DarkGray }

$results = New-Object System.Collections.Generic.List[Object]
$counter = 0

foreach ($gpo in $allGpos) {
    $counter++
    Write-Progress -Activity "Scanning GPOs" -Status $gpo.DisplayName `
        -PercentComplete (($counter / $allGpos.Count) * 100)

    try {
        $xml = Get-GPOReport -Guid $gpo.Id -ReportType Xml
    }
    catch {
        Write-Warning "Failed to get report for '$($gpo.DisplayName)': $_"
        continue
    }

    if ($ExportPath) {
        $safeName = $gpo.DisplayName -replace '[\\/:*?"<>|]', '_'
        $xml | Out-File -FilePath (Join-Path $ExportPath "$safeName.xml") -Encoding utf8
    }

    foreach ($pattern in $patterns) {
        $regexOptions = if ($CaseSensitive) {
            [System.Text.RegularExpressions.RegexOptions]::None
        } else {
            [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
        }

        $hits = [regex]::Matches($xml, $pattern, $regexOptions)

        if ($hits.Count -gt 0) {
            $matchedLines = @()
            if ($ShowContext) {
                $lines = $xml -split "`r?`n"
                $lineRegex = New-Object System.Text.RegularExpressions.Regex($pattern, $regexOptions)
                $matchedLines = $lines | Where-Object { $lineRegex.IsMatch($_) } | ForEach-Object { $_.Trim() }
            }

            $results.Add([PSCustomObject]@{
                GPOName    = $gpo.DisplayName
                GPOId      = $gpo.Id
                Pattern    = $pattern
                HitCount   = $hits.Count
                MatchLines = ($matchedLines -join " | ")
            })
        }
    }
}

Write-Progress -Activity "Scanning GPOs" -Completed

if ($results.Count -eq 0) {
    Write-Host "No matches found." -ForegroundColor Yellow
    return
}

Write-Host "`nFound $($results.Count) matching GPO/pattern combination(s):" -ForegroundColor Green

if ($ShowContext) {
    $results | Format-Table GPOName, Pattern, HitCount, MatchLines -Wrap -AutoSize
} else {
    $results | Format-Table GPOName, GPOId, Pattern, HitCount -AutoSize
}

# Also return the objects to the pipeline so you can pipe into
# Export-Csv, Out-GridView, etc.
$results

But there’s an issue when running a new script. Depending on the execution policy set on your machine, PowerShell either blocks all unsigned scripts, or refuses to run any script at all

Bypass or Disable Script Signing Verification

One way would be to change the execution policy away from RemoteSigned in PowerShell, but I didn’t want that.

Here are the PowerShell commands for that.

Bypass for this PowerShell session only

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process

Bypass for a single script

powershell.exe -ExecutionPolicy Bypass -File .\script.ps1

Change the setting for the user scope (persistent)

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser

So, what’s the alternative? Signing the scripts ourselves.

Signing the Script

Create certificate template

We have a self-hosted PKI running at work, so I will use this. But you could also just use a locally generated self-signed certificate. Just keep in mind that other machines will only show a self-signed certificate’s signature as valid if that certificate is imported into their Trusted Root Certification Authorities (or Trusted Publishers) store. That’s not an issue for us since our PKI’s root is already trusted domain-wide via GPO.

Let’s begin by duplicating the “Code Signing” template.

Certificate Templates Console: duplicating the Code Signing template
Certificate Templates Console: duplicating the Code Signing template

In the new template I set the following options.

General tab: template name and validity/renewal period
General tab: template name and validity/renewal period
Compatibility tab: certification authority and certificate recipient OS versions
Compatibility tab: certification authority and certificate recipient OS versions
Cryptography tab: RSA key with a 4096-bit minimum size and SHA256 request hash
Cryptography tab: RSA key with a 4096-bit minimum size and SHA256 request hash
Subject Name tab: building the subject name from Active Directory
Subject Name tab: building the subject name from Active Directory

Generate a certificate

Now that we have our code signing template, we can generate a certificate for our user.

Certificate Manager (certmgr.msc): requesting a new certificate
Certificate Manager (certmgr.msc): requesting a new certificate
Certificate enrollment: selecting the Code Signing certificate template
Certificate enrollment: selecting the Code Signing certificate template

Alright. Export the certificate (or if it’s the workstation you want to sign your code, just leave it) and save it in a safe location where you have access from your workstation.

Now we can start signing our scripts.

Code Signing

I have a script that searches the GPOs for a specific keyword. In case we set some registry entry or desktop icon manually, I can search for it with one command, rather than searching each and every policy.

Anyway. Let’s say we want to sign this specific PowerShell script.

Navigate to the script folder.

When using the certificate store

# Use if you have a single signing certificate
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert

# Use if you have multiple signing certificates
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1

# Sign the Script
Set-AuthenticodeSignature -FilePath ".\Search-GPOKeyword.ps1" -Certificate $cert -HashAlgorithm SHA256 -TimestampServer "http://timestamp.digicert.com"

# Verify
Get-AuthenticodeSignature -FilePath ".\Search-GPOKeyword.ps1"
SignerCertificate                         Status                    StatusMessage             Path
-----------------                         ------                    -------------             ----
1221EF7FC95A154B3A57C3866F0D7EF50A0900B87  Valid                     Signatur überprüft.       Search-GPOKeyword.ps1

If it’s not signed, the message will look like this.

SignerCertificate                         Status                    StatusMessage             Path
-----------------                         ------                    -------------             ----
                                          NotSigned                 Die Datei „C:\Users\tuez… Search-GPOKeyword-no-sig…

When using a certificate file

# store the pfx password
$password = Read-Host -AsSecureString "<enter the pfx password>"

# store the certificate
# Note: -Password requires PowerShell 6+ (pwsh). In Windows PowerShell 5.1,
# Get-PfxCertificate has no -Password parameter — omit it there and it
# will prompt you interactively instead.
$cert = Get-PfxCertificate -FilePath "C:\path\to\Codesignatur.pfx" -Password $password

# Sign the script
Set-AuthenticodeSignature -FilePath ".\Search-GPOKeyword.ps1" -Certificate $cert -HashAlgorithm SHA256 -TimestampServer "http://timestamp.digicert.com"

That is pretty much it.

I hope this helps. It was definitely easier to sign a script than I thought.

Till next time.

Comments

Search