Signing PowerShell scripts

A PowerShell script is plain text — anyone can open Deploy.ps1 in Notepad and change what it does. Signing a PowerShell script with a code signing certificate does two things: it proves which publisher produced the script, and it lets Windows detect if a single byte has changed since it was signed.

Windows PowerShell 5.1 and PowerShell 7 both enforce this through the execution policy. Under the AllSigned or RemoteSigned policies, an unsigned — or tampered — script is refused. Signing your scripts is what lets them run on locked-down machines.

Signotaur signs PowerShell scripts exactly the same way it signs an .exe: there is no separate command and no PowerShell-specific option. The signing certificate's private key stays on the Signotaur server and never touches the build machine.

Why sign a PowerShell script

  • Identity — recipients can see the script came from you and not an impostor.
  • Integrity — any modification after signing invalidates the signature, so tampering is detected.
  • Execution policy compliance — the RemoteSigned and AllSigned policies refuse to run unsigned scripts. If your scripts run on servers or managed desktops, they almost certainly need to be signed.
  • Application control — in environments locked down with AppLocker or Windows Defender Application Control (WDAC), a publisher rule can allow your signed scripts while blocking everything else.

What can be signed

PowerShell signing is Authenticode signing — the same technology used for .exe and .dll files. SignotaurTool.exe sign detects these file types automatically by extension:

Extension File type
.ps1 PowerShell script
.psm1 PowerShell script module
.psd1 PowerShell module manifest / data file
.ps1xml PowerShell format and type definition data
.cdxml Cmdlet definition XML (CDXML) module

Because these are known file types, no --unsupported-file-types flag is needed — Signotaur routes them through Authenticode signing automatically.

Signing a PowerShell script

Use the sign command with the script path. This is the same command you would use for an .exe — there are no PowerShell-specific flags:

SignotaurTool.exe sign -a <APIKey> -s <SignServer> -t <Thumbprint> --fd SHA256 --tr http://timestamp.digicert.com --td SHA256 Deploy.ps1

To sign every script in a module or repository, use wildcards:

SignotaurTool.exe sign -a <APIKey> -s <SignServer> --label production --fd SHA256 --tr http://timestamp.digicert.com --td SHA256 src\**\*.ps1 src\**\*.psm1

The other common sign options work as usual. See the sign command reference for full details. For CI/CD pipelines, prefer --label over --thumbprint so a renewed certificate is picked up automatically without editing your build scripts.

What a signed script looks like

Authenticode signs a PowerShell file by appending the signature as a comment block at the very end of the file:

# SIG # Begin signature block
# MIIm... (base64-encoded PKCS#7 signature data)
# SIG # End signature block

Every line in the block starts with #, so PowerShell treats it as a comment and the script runs exactly as before — but Windows reads the block when the execution policy validates the script. Re-signing a script simply replaces the existing block; you do not need to remove it first.

PowerShell execution policy and signing

Signing a script only has an effect if the machine that runs it has an execution policy that requires a signature. Check the current policy:

Get-ExecutionPolicy -List

The two policies that require signed scripts are:

  • RemoteSigned — scripts downloaded from the internet or a network share must be signed; scripts created locally may run unsigned. A common default on Windows Server.
  • AllSigned — every script must be signed by a trusted publisher, including local ones. The strictest setting.

Set a policy (the LocalMachine scope requires an elevated prompt):

Set-ExecutionPolicy RemoteSigned -Scope LocalMachine

Signing the script is only half the job — the machine that runs it must trust the certificate that signed it.

If the certificate chains to a commercial root (DigiCert, Sectigo, GlobalSign, …), that trust is already present on every Windows machine and no recipient action is needed.

If it chains to an internal or self-signed CA, import the CA certificate into the machine's Trusted Root Certification Authorities store. Under AllSigned, also import the signing certificate into Trusted Publishers — otherwise the first run prompts "Do you want to run software from this untrusted publisher?". Without trusted chain, a correctly signed script is still refused with an "is not digitally signed" error.

Verifying a signed PowerShell script

PowerShell has a built-in cmdlet for checking a script's signature:

Get-AuthenticodeSignature .\Deploy.ps1

A Status of Valid means the signature is intact and the publisher is trusted on this machine. A status of NotTrusted or UnknownError usually means the signing certificate's chain is not trusted on the machine doing the check — see the note above.

You can also verify with SignotaurTool, which additionally checks the certificate chain and any embedded timestamp:

SignotaurTool.exe verify Deploy.ps1

See the verify command reference for full syntax.

Timestamping

Always pass a timestamp server (--tr) and digest (--td) when signing — the examples above already do. Timestamping matters for scripts that need to keep working over time:

  • Without a timestamp, a signed script stops validating the moment the signing certificate expires. Under AllSigned it will no longer run at all.
  • With an RFC 3161 timestamp, Windows treats the signature as valid past certificate expiry, because the timestamp proves the script was signed while the certificate was still valid.

Unlike .rdp files — where mstsc.exe ignores the timestamp — PowerShell does honour the embedded timestamp, so a timestamped script keeps running after its certificate expires.

Certificate requirements

The signing certificate must carry the Code Signing Enhanced Key Usage (EKU). PowerShell signing uses Authenticode, so a standard RSA code-signing certificate works; an ECDSA certificate also works on Windows 8 and later. For algorithm choice, key sizes, and the CA/Browser Forum minimum key size for publicly-trusted certificates, see Code Signing Certificates.

Troubleshooting

Symptom Likely cause Fix
Deploy.ps1 cannot be loaded. The file Deploy.ps1 is not digitally signed. The script is unsigned, was modified after signing, or the signing certificate's chain is not trusted on this machine Sign the script; if it is already signed, import the issuing CA into Trusted Root Certification Authorities
... cannot be loaded because running scripts is disabled on this system The execution policy is Restricted This is not a signing problem — set an appropriate policy with Set-ExecutionPolicy
Get-AuthenticodeSignature reports Status: NotTrusted The signing certificate or its chain is not trusted on the machine running the check Import the certificate / CA chain into the trust stores; under AllSigned, add the signer to Trusted Publishers
A script that worked is refused months later The signing certificate expired and the script was signed without a timestamp Re-sign with --tr/--td; always timestamp
Do you want to run software from this untrusted publisher? prompts on every run Under AllSigned, the publisher is not in Trusted Publishers Choose Always run, or pre-import the signing certificate into the Trusted Publishers store

References

  • Sign command reference
  • Verify command reference
  • Code Signing Certificates
  • Migrating from SignTool to SignotaurTool
  • Microsoft Learn: about_Signing