Powershell Script Signing

Published:
Updated:
How to sign a powershell script so you can prevent tampering, and only allow users to run authorised Powershell scripts

Introduction


In today's security conscious world, we - as Administrators - need to be security conscious too.  This means, taking all the necessary steps to safeguard our users and systems.  Powershell Scripts - while not new - is still something that a lot of people use in a way that's not entirely safe.  Powershell's default execution policy - "Restricted" pretty much means "You can't run any scripts at all".  Naturally, we want to run scripts, so we change this - but what do we change it to?  "Unrestricted" of course - and that there is the problem.  We've just disabled a good security feature.
 

Execution Policies


The possible execution policies in Powershell are as follows:
 
  • Restricted: Does not load configuration files or run scripts. "Restricted" is the default execution policy.
  • AllSigned: Requires that all scripts and configuration files be signed by a trusted publisher, including scripts that you write on the local computer.
  • RemoteSigned: Requires that all scripts and configuration files downloaded from the Internet be signed by a trusted publisher.
  • Unrestricted: Loads all configuration files and runs all scripts. If you run an unsigned script that was downloaded from the Internet, you are prompted for permission before it runs.
  • Bypass: Nothing is blocked and there are no warnings or prompts.
  • Undefined: Removes the currently assigned execution policy from the current scope. This parameter will not remove an execution policy that is set in a Group Policy scope.

Seeing what the current execution policy is can be done in Powershell by issuing the command Get-ExecutionPolicy, and setting it is done via Set-ExecutionPolicy.  See the following screenshots for examples:

01---GetExecutionPolicy-Sample.pngThe two safest policies are: "RemoteSigned" and "AllSigned".  This guide will help you sign your scripts so that your scripts will be allowed to run under these two policies.
 

Proper PKI


In order to complete this guide, you do need to have a Certificate Authority.  Certificate Authority is pretty much a required component of any reasonable size corporation.  It's free with Windows Server, and can be deployed on a domain controller, or on a member server.  Prior to continuing this guide, please find your Certificate Authority, or create one for your organization.  This article will not cover using self-signed certificates, for the mere reason that self-signed certificates should not be used in a corporate environment.  We're trying to make things secure - let's do it properly :)  This guide will not cover creation of a Certificate Authority, but assuming one does not exist at EE, I'll create one.

Tip: Running the following command will bring up a box showing you which server is your Certificate Authority.
 
certutil -config - -ping

Open in new window

 

Enable Code Signing Template


The first piece of the puzzle is to enable the Code Signing template, which is not enabled by default.  Log on to your Certificate Authority as your Enterprise Administrator (or equivalent), open up the Certificate Authority administration snap-in, and then locate and select the "Certificate Templates" node.  Right click the node and then select "New" and then "Certificate Template to Issue"

02---New-Certificate-Tempalte-to-Iss.pngSelect "Code Signing" from the drop down list, and then click OK.  If "Code Signing" appears in the Certificate Templates window, then you're finished here - and you can log off the server.
 

Request Code Signing Certificate


On your computer, open up the Certificate Manager.  TIP: You can open this up easily by using Windows+R to open up the Run prompt and typing "certmgr.msc"  Once here, navigate to your Personal Certificates folder, and right click it, and select "All Tasks" and then "Request New Certificate..."

03---Request-New-Certificate.pngClick "Next" at the Before you Begin page, and then ensure that "Active Directory Enrollment Policy" is selected on the Select Certificate Enrollment Policy page, before clicking Next.

04---Active-Directory-Enrollment-Pol.pngCheck the "Code Signing" box, and then click "Enroll"

05---Enroll-for-Code-Signing.pngYou should see a Success.  You can use the drop down arrow next to "Details" to see more details about the certificate.

06---Enrollment-Success.pngWhen you are finished with that, you should now see your certificate as part of your personal certificate store.

07---My-Certificate.pngWhile you're at it, let's have a look at your certificate store using Powershell.  You will need this command for later anyway!  Use the following command:
 
Get-ChildItem cert:\CurrentUser\My -codesign

Open in new window


And it will output information similar to this:

08---Viewing-My-Code-Signing-Certifi.png

Signing your Powershell Script


If you view your powershell script right now, you'll see that it's fairly plain - and exactly what you typed originally.

09---Content-of-my-Script.pngI can run this script right now, because my script is local to my computer.  "RemoteSigned" means that it only has to be signed if I'm running it from a remote location, such as a network share.  The thing is, I want to share this script with all my corporate users, so it's better that it's in a central place.

We are now going to sign the script, using this command line:
 
Set-AuthenticodeSignature <filename> <SignerCertificate>

Open in new window


For me, it's this:
 
Set-AuthenticodeSignature .\myscript.ps1 @(Get-ChildItem cert:\CurrentUser\My -codesign)[0]

Open in new window


You will notice for SignerCertificate portion, I've re-used an earlier command I did.  One thing that's different though is that I've specified an index at the back ([0] refers to the first item in the array).  If you have multiple code signing signatures, you may need to change the index accordingly.

If all goes well, this is the output you should see:

10---Set-AuthenticodeSignature-outpu.pngAnd now, if we have a look at our script, it will look a bit more interesting:

11---Content-of-my-Script-Now.png


Running the Script


I'm now going to set my Execution Policy to "AllSigned", so that I'm going to force my script execution to require signing, even when running locally.

When Powershell has been told to require signatures, the first thing it's going to do is see if we've previously trusted the code signer. Despite the fact that we have a proper PKI in place, with a Certificate Authority that is trusted, it still wants the user to approve the code signer.  When you run the script, you will receive a prompt similar to the following:

Do you want to run software from this untrusted publisher?
File C:\Users\lclayton\myscript.ps1 is published by CN=Lester Clayton, OU=Users, OU=Support Accounts, DC=mgmt, DC=local and is not trusted on your system. Only run scripts from trusted publishers.
[V] Never run  [D] Do not run  [R] Run once  [A] Always run  [?] Help (default is "D"):
"R" will run the script once, but not add the code signer to trusted publishers, meaning that if I run the script again, I'll get the same prompt.
"A" will add the code signer to the users trusted publishers store, and then they'll never get prompted again for any scripts that are signed by that Code Publisher.

Had you selected "A", then if you view your Trusted Publishers Store now, you'll see that the certificate has appeared.  I am now a trusted publisher.

12---Trusted-Publishers.png


Automating Trusting Code Publishers


Of course, we don't want our users to be nagged by these prompts.  Using a Group Policy Object, it is possible to pre-install certificates for users inside an Active Directory Domain.  The GPO would look similar to this:

13---GPO-for-Authorizing-Trusted-Pub.png


Tips & Tricks


Just remember that by doing the right thing and enforcing code signing, you are going to have to commit to a bit more maintenance.  Certificates expire over time, and with each modification you make to your script, you are going to have to re-sign the certificate, otherwise the certificate validation will fail.

The default validity period for the Code Signing template is 1 year.  If you duplicate that template, you can set the validity period to be even longer.  Your Certificate Authority has most likely got a long validity period (5 years or more), so you can set the validity period to be the same.  NOTE: You cannot have an issued certificate validity longer than your CA's current validity period - so you can't issue a certificate for 99 years when your CA expires in 3!

Using Powershell, you could sign all the scripts in a folder with the same signature by using something similar to the following:
 
$MySignature = @(Get-ChildItem cert:\CurrentUser\My -codesign)[0]
                      Get-ChildItem "\\mgmt.local\SYSVOL\mgmt.local\scripts" -Filter *.ps1 | 
                      Foreach-Object { Set-AuthenticodeSignature $_.FullName $MySignature }

Open in new window


Don't forget that you can also force an execution policy through Group Policies, so that users (or even other Administrators) can't change their Execution Policy back to "Bypass"

This extra maintenance is worth the effort, to bring you peace of mind:
  • Users will not be able to modify your scripts that you've signed and run them
  • Users will not be able to run scripts which they've downloaded from the Internet (unless they've been signed by a trusted Certificate Authority - which is unlikely!)
  • Your Administrator "friends" can't tamper with your code making Troll changes for a laugh.
I hope this guide has been of use to you - protect your code today!
9
43,943 Views

Comments (6)

Author

Commented:
Thanks for the kind words - I like to show pictrures so that people can follow the important steps :)
great article and very helpful thankyou.
quick question, am i able to sign my ps' from an enterprise PKI so i do not have to 'trust' the publisher as we already trust the root,intermediate etc from gpo?

thanks again

Author

Commented:
To Carl Sciberras:

Thank you for your compliment.  Yes, you can use an internal PKI, however it's not enough to trust the root certificate - users still have to trust each developer.  You can automate this via GPO, just look at the "Automating Trusting Code Publishers" section above.
thanks Lester for that,
yes i have since added myself (publisher) to trusted publishers from the gpo and it works perfectly.

thankyou again !

Commented:
Two things:
1. Script signatures can be time stamped (by external providers with a timestamping server) so that if the signing certificate was valid at the time of signing then the script will continue to run even after the signing certificate has expired.
2. The fact that PowerShell prompts when an untrusted publisher is encountered and provides options [A] or [R] that will run the script anyway ([A] will also add the certificate to trusted publishers) undermines security.  There is no Group Policy setting that I know of that can stop the prompt or mandate one of the four choices.  What is the point of an AllSigned PowerShell execution policy when any user with a script signed by an unauthorised code signing certificate (e.g. self-signed) can answer [A] or [R] and have it run regardless.

View More

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.