PowerShell Signing Scripts: Code Sign Your .ps1 Files

PowerShell Signing Scripts: Code Sign Your .ps1 Files

PowerShell Tips Editor 5 min read
PowerShell Signing Scripts: Code Sign Your .ps1 Files

Enterprise environments running the AllSigned execution policy require every PowerShell script to carry a valid digital signature before it runs. Unsigned scripts are blocked outright, even from the console. Learning to PowerShell sign script files with a code signing certificate is a prerequisite for deploying scripts in these environments. This post covers obtaining certificates, signing scripts, verifying signatures, and fitting signing into a CI pipeline.

Quick Answer / TL;DR

Get a code signing certificate, then run Set-AuthenticodeSignature -FilePath .\script.ps1 -Certificate $cert. Verify with Get-AuthenticodeSignature .\script.ps1. Any modification after signing invalidates the signature.

Get a Code Signing Certificate

Code signing certificates come from two sources: your organization’s internal PKI (via Active Directory Certificate Services) or a public commercial CA. The certificate must have the Code Signing enhanced key usage (EKU). Retrieve certificates already installed in your certificate store with Get-ChildItem against the Cert: drive.

# Find code signing certificates in your personal store
$certs = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
$certs | Select-Object Subject, Thumbprint, NotAfter

# Select the first available
$cert = $certs | Select-Object -First 1
Write-Host "Using: $($cert.Subject)"

Create Self-Signed Certificate for Testing

During development and testing, use a self-signed certificate. Self-signed certs are not trusted on other machines by default, but they let you test the signing workflow before deploying with a CA-issued certificate. The New-SelfSignedCertificate cmdlet creates the certificate and installs it in the specified store.

# Create self-signed code signing certificate (requires admin for LocalMachine store)
$cert = New-SelfSignedCertificate `
    -Subject 'CN=PowerShell Dev Signing' `
    -CertStoreLocation Cert:\CurrentUser\My `
    -KeyUsage DigitalSignature `
    -Type CodeSigningCert `
    -NotAfter (Get-Date).AddYears(2)

Write-Host "Created certificate: $($cert.Thumbprint)"

# For the cert to be trusted locally during testing, add to Trusted Publishers
$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser')
$rootStore.Open('ReadWrite')
$rootStore.Add($cert)
$rootStore.Close()

Sign a Script with Set-AuthenticodeSignature

Set-AuthenticodeSignature appends a cryptographic signature block to the end of the script file as a comment. The signature covers the entire file content — any character change after signing invalidates it. The -TimestampServer parameter adds a trusted timestamp, which keeps the signature valid even after the signing certificate expires.

# Sign a script with timestamp (production best practice)
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1

Set-AuthenticodeSignature `
    -FilePath C:\Scripts\Deploy-App.ps1 `
    -Certificate $cert `
    -TimestampServer 'http://timestamp.digicert.com'

# Verify the result
$sig = Get-AuthenticodeSignature -FilePath C:\Scripts\Deploy-App.ps1
Write-Host "Status: $($sig.Status)"
Status: Valid

Verify Script Signature

Get-AuthenticodeSignature returns a signature object with a Status property. Valid means the signature is intact and the certificate chains to a trusted root. HashMismatch means the file was modified after signing. NotTrusted means the certificate is not in the trust chain.

# Check a single script
$sig = Get-AuthenticodeSignature -FilePath C:\Scripts\Deploy-App.ps1
$sig | Select-Object Path, Status, SignerCertificate

# Batch-check all scripts in a folder
Get-ChildItem C:\Scripts -Filter *.ps1 | ForEach-Object {
    $s = Get-AuthenticodeSignature $_.FullName
    [PSCustomObject]@{
        File   = $_.Name
        Status = $s.Status
        Signer = $s.SignerCertificate.Subject
    }
} | Format-Table -AutoSize

Automate Signing in CI Pipeline

In a CI/CD pipeline (Azure DevOps, GitHub Actions), install the signing certificate from a secure vault as part of the build, sign all scripts, then verify before deployment. Use a machine-level certificate store in the pipeline agent for consistent access.

# CI pipeline signing step — certificate thumbprint from pipeline variable
$thumbprint = $env:SIGNING_CERT_THUMBPRINT
$cert = Get-Item "Cert:\LocalMachine\My\$thumbprint"

Get-ChildItem -Path '.\src' -Filter '*.ps1' -Recurse | ForEach-Object {
    $result = Set-AuthenticodeSignature `
        -FilePath $_.FullName `
        -Certificate $cert `
        -TimestampServer 'http://timestamp.digicert.com'

    if ($result.Status -ne 'Valid') {
        throw "Signing failed for $($_.Name): $($result.Status)"
    }
    Write-Host "Signed: $($_.Name)"
}

AllSigned Execution Policy Workflow

With AllSigned policy, PowerShell refuses to run any unsigned or invalidly-signed script. The policy is scoped to a specific level: Process, CurrentUser, or LocalMachine. Verify the effective policy before deployment to ensure signed scripts will actually run in the target environment.

# Check effective execution policy
Get-ExecutionPolicy -List

# Set AllSigned for the current machine (requires admin)
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine -Force

# Test that a signed script runs correctly
powershell -ExecutionPolicy AllSigned -File C:\Scripts\Deploy-App.ps1

Common Errors and Fixes

  • Self-signed cert not trusted on other machines by default. Self-signed certificates do not chain to any trusted root. On the signing machine, add the cert to Cert:\LocalMachine\TrustedPublisher and Cert:\LocalMachine\Root. On other machines, deploy the cert via Group Policy or distribute it manually. For production, always use a CA-issued certificate.
  • Script modification after signing invalidates the signature. Even a single space or newline change causes Get-AuthenticodeSignature to return HashMismatch. Always sign as the last step before deployment, never before finalizing the script content.

Related Cmdlets / See Also

Wrapping Up

Code signing is a one-time setup investment that makes your scripts deployable in every enterprise environment. Use New-SelfSignedCertificate for local development, a CA-issued certificate for production, always include a -TimestampServer, and verify with Get-AuthenticodeSignature as the final step in your CI pipeline.

Send-Item -To