Signing PowerShell scripts
Warren
Posted on March 6, 2020
Signing PowerShell scripts
I ❤️ PowerShell. It's a really useful tool for automating those tasks you do multiple times. But it can also be a gaping security hole if you let it.
PowerShell allows you to administer almost everything on your machine, so there is a lot of damage that could be done by someone able to run malicious scripts in your environments.
Previously confined to just Windows, since version 6 and now with the release of PowerShell 7.0, it can also be deployed on Linux and MacOS. However this article talks about Execution Policies which cannot be changed in non Windows environments so will provide no benefit to Linux/MacOS users (sorry).
Execution Policies
One way you can restrict the ability to run scripts in your Windows environments is to use PowerShell's execution policies. The tldr is that they can be used to restrict the scripts that will run in the environment. The options available from least secure to most secure are:
Bypass
Nothing is blocked.
Unrestricted (Always applies on Non-Windows machines)
Same as ByPass
but prompts the user before running scripts from the internet.
RemoteSigned (default for Windows Servers)
Scripts that have been downloaded from the internet can only be run if they are signed. Scripts written on the machine can be run
AllSigned
All scripts must be signed before they will run
Restricted (default for Windows Clients)
Prevents running scripts. Can only run individual commands.
Running Scripts
So you want to run your own PowerShell scripts on your server. But you're getting a PSSecurityException
like the following.
PS C:\Users\Administrator\Downloads> .\wibble.ps1
.\wibble.ps1 : File .\wibble.ps1 cannot be loaded. The file
.\wibble.ps1 is not digitally signed. You cannot run this script on the current system.
For more information about running scripts and setting execution policy, see about_Execution_Policies at
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\wibble.ps1
+ ~~~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
One way to resolve this issue is to change the execution policy using the Set-ExecutionPolicy
command and reduce the security of the machine. This is a generally a bad idea.
Personally, I would never use anything less secure than RemoteSigned
on a server, but AllSigned
ideally.
What is a script signature?
Presumably you want to maintain the security of the machine and not open up a vulnerability so the option left to you is to sign your scripts.
Signing a script means that someone with access to the private key for a code signing certificate has added a signature block at the end of the script file.
function Get-Wibble {
return "Wibble"
}
# SIG # Begin signature block
# vpnIUpm2XxLRhU1no0iuA62xKxYzR6m95z9Ax21ppeTC9NoRd8ocoSGr1zAd
# qlMOlz4lZoVWR4ZmtdCgzde1dVxzv4jjHb6ziDiY2o05UXswD2bl6XaOrUpd
# Li0Qjg3d3y2r1nrpO8hos906bgXQswysvouegUJcpt8ftmqBKfEYNeBgnBFm
# SIG # End signature block
(Example only. The signature block is usually much longer)
The signature block will contain a hash of the script that has been encrypted using the private key. When attempting to run the script this is decrypted using the public key and compared to the actual hash. If it matches then we can confirm that the script has not been tampered with as the hash would change as soon as that happened.
Self Signing certificates
If your scripts are only going to be run by machines in your organisation then you will most likely be able to self sign the certificates. The alternative is to spend $$$ and buy a code signing certificate each year.
Self signing means you will generate the certificate yourself and sign the scripts using that.
How to create a self signing certificate
PowerShell has the very useful New-SelfSignedCertificate
command for producing self signed certificates. We can then export them and place them in different stores.
By default the command will create a certificate that expires after 1 year. You can change this using the -NotAfter
parameter and providing it the date you wish the certificate to expire.
To create a certificate fire up a PowerShell session and run the following
$CertificateName = Read-Host "Input your certificate name"
$OutputPFXPath = "$CertificateName.pfx"
$OutputCERPath = "$CertificateName.cer"
$Password = Get-Credential -UserName Certificate -Message "Enter a secure password:"
$certificate = New-SelfSignedCertificate -subject $CertificateName -Type CodeSigningCert -CertStoreLocation "cert:\CurrentUser\My"
$pfxCertificate = Export-PfxCertificate $certificate -FilePath $OutputPFXPath -password $Password.password
Export-Certificate -Cert $certificate -FilePath $OutputCERPath
Import-PfxCertificate $pfxCertificate -CertStoreLocation cert:\CurrentUser\Root -Password $password.password
Write-Output "Private Certificate '$CertificateName' exported to $OutputPFXPath"
Write-Output "Public Certificate '$CertificateName' exported to $OutputCERPath"
This will create the certificate and export it to two separate files with the extensions .pfx
and .cer
.
PFX
The PFX certificate is the one that will need to be installed on the machines that will be doing the signing. In our case that is each of our developers machines.
It should be installed in to the cert:\CurrentUser\Root
store, also known as the "Trusted Root Certification Authorities" store.
This file should be kept somewhere secure. The password should then be secured separately (use a password manager). If a bad actor gets hold of both of these then they can sign their malicious scripts as if they were you.
CER
The CER certificate file is what you're going to have to install on the machines that will be running the scripts. This won't require a password to install but cannot be used to sign scripts. It only contains the public key used to decrypt the signature.
Because it doesn't contain the private key this certificate can be freely distributed to all environments that will be running the scripts.
This will need to be installed in two certificate stores on the machines that will be running the scripts; The cert:\LocalMachine\Root
and cert:\LocalMachine\TrustedPublisher
stores.
Signing a script
The Set-AuthenticodeSignature
function is provided to sign the scripts. We need to pass in the path to the script and the certificate from the store. We wrote a wrapper function that can sign the scripts.
function Set-ScriptSignatures {
param (
[string]
$pathToScripts = "."
)
$certificateName = "PowerShell Signing Certificate"
$scripts = Get-ChildItem -Path "$pathToScripts\*" -Include *.ps1, *.psm1 -Recurse
$certificate = @(Get-ChildItem cert:\CurrentUser\My -codesign | Where-Object {
$_.issuer -like "*$certificateName*" } )[0]
foreach ($script in $scripts)
{
Write-Host "Signing $script"
Set-AuthenticodeSignature $script -Certificate $certificate
}
}
The $certificateName
variable should contain the name used when creating the certificate.
You can use the function by providing a path to the directory that contains the scripts and it will loop through them signing them all.
Once you've installed the scripts on the target machine you should now be able to run the run the scripts without any issues.
Posted on March 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.