Signing your PowerShell Scripts
At some point or another, we have all been in the situation, usually on a new machine, we get the familiar PSSecurityException.
.\xxx.ps1 : File C:\xxx.ps1 cannot be loaded. The file C:\xxx.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 + .\xxx.ps1 + ~~~~~~~~~~ + CategoryInfo : SecurityError: (:) [], PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess
Of course we know this is down to PowerShell Policy. The behaviour of PowerShell when it executes a script can be configured to one of the following:
- Restricted – This is the most secure but the least versatile. It does not allow any scripts to be run.
- AllSigned – This allows scripts that have been digitally signed by a certificate that the computer trusts to run.
- Unrestricted – This is the least secure. It allows all scripts regardless of origin to run.
- RemoteSigned – This is a step up from Unrestricted. Scripts that have been downloaded are not allowed to run unless they are digitally signed by a certificate that the computer trusts.
The default policy depends on a number of things including group policy and which operating system you are using. A lot of people get around this by setting the execution policy to Unrestricted.
Set-ExecutionPolicy Unrestricted
I however do not like this as it leaves your system open to abuse.
RemoteSigned should be the minimum you ever set this policy to. Even with RemoteSigned you may find you still get the above error when your script is located on a network drive. In this case rather than changing the execution policy to Unrestricted you should change your internet options so the server that the file share is hosted on is trusted. To do this you should go to Control Panel and Internet Options to open the Internet Options dialog:
Under Internet Options select the Security tab and then the Local Intranet zone. Once selected click Sites then Advanced. Add the server to the list in the form file://<servername> (untick Require server verification). Once this has been updated restart any PowerShell windows and try executing your script again on a network share and you should find they run.
However I think the best approach is to sign your scripts. The argument against this is that it’s a “complicated” process involving certificates which people seem to find hard anyway.
It is true that you will need to get a code signing certificate and if your IT Support has set things up correctly you should be able to get a code signing certificate in about 30 seconds (see Create a Code Signing Certificate in 30 Seconds). Sometimes your IT Support will not have configured your infrastructure for maximum usability and security and so you may need to expend a bit more effort, however this is an infrequent process with big advantages for security.
If you have a code signing certificate then signing your scripts is incredibly easy. Just substitute the certificate thumbprint with your own:
$cert = Get-ChildItem cert:\CurrentUser\My\FACE9812CAFE7634BABE54561A2B3C4D5E6FDEAD Set-AuthenticodeSignature -Certificate $cert -FilePath X:\path_to_script.ps1
This will add a block like the following to the end of your script:
# SIG # Begin signature block # MIIInAYJKoZIhvcNAQcCoIIIjTCCCIkCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB ...................... # NNDblcNY9HDwe+tXuDRtu3YLoBWBq4xasQTml46HHtEd1z6L+qTDi1gwbv/dFmIB # SIG # End signature block
This is the digital signature and will be what PowerShell verifies when trying to run a script. See how easy that was?
Top tip
My home directory is hosted on a network server and so my PowerShell profile file is hosted on the network share. This means by default, without signing my PowerShell profile, every time I load PowerShell I will get a message:
. : File \\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded. The file \\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Microsoft.PowerShell_profile.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:3 + . '\\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Micr ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : SecurityError: (:) [], PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess
It was easy enough to sign my profile file:
$cert = Get-ChildItem cert:\CurrentUser\My\FACE9812CAFE7634BABE54561A2B3C4D5E6FDEAD Set-AuthenticodeSignature -Certificate $cert -FilePath $profile
The problem is that I frequently add, remove or edit things in my profile which usually leads to the following error message:
. : File \\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded. The contents of file \\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 might have been changed by an unauthorized user or process, because the hash of the file does not match the hash stored in the digital signature. The script cannot run on the specified system. For more information, run Get-Help about_Signing.. At line:1 char:3 + . '\\domain.com\FILES\USERS\my.username\My Documents\WindowsPowerShell\Micr ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : SecurityError: (:) [], PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess
This is due to the file being changed so the digital signature is no longer valid. It’s easy enough to correct by signing the file again:
$cert = Get-ChildItem cert:\CurrentUser\My\FACE9812CAFE7634BABE54561A2B3C4D5E6FDEAD Set-AuthenticodeSignature -Certificate $cert -FilePath $profile
but I find this tedious as looking up my thumbprint and then typing the path is more effort than I’m willing to spend. I always usually have a PowerShell window open even when I am editing my profile and so to save me some time I have defined the following function in my profile:
function Update-ProfileSignature() { $signingCertificateThumbprint = (Get-AuthenticodeSignature $profile).SignerCertificate.Thumbprint $codeSigningCertificate = Get-ChildItem -Recurse Cert:\ | ? { $_.Thumbprint -eq $signingCertificateThumbprint -and $_.HasPrivateKey } | Select -First 1 Set-AuthenticodeSignature -FilePath $profile -Certificate $codeSigningCertificate | Out-Null Write-Host "Updated signature on profile" -ForegroundColor Green }
Everytime I edit my profile, in an already open PowerShell window, I execute the function Update-ProfileSignature
which re-signs my profile file.
It’s important that a PowerShell window is already open (and therefore has the Update-ProfileSignature function loaded and defined) as once you edit your profile you cannot load your profile without error into any new PowerShell windows until it is re-signed and therefore you cannot call Update-ProfileSignature
until it is re-signed, which would make it useless.