Run Scripts Securely With Execution Policy and Signing
PowerShell's Execution Policy is a core security feature designed to control how scripts are loaded and run. It's often misunderstood as a security…
PowerShell's Execution Policy is a core security feature designed to control how scripts are loaded and run. It's often misunderstood as a security boundary, but it's primarily a safety mechanism. Coupled with Authenticode digital signing, it forms a robust framework for managing script execution in corporate environments, preventing accidental execution of untrusted code and providing an audit trail for script origin.
This article details the various execution policies, best practices for their application in different scenarios, and a practical guide to signing your own PowerShell scripts using an internal Certificate Authority (CA) to facilitate secure script deployment.
Understanding PowerShell Execution Policies
Execution Policies are client-side settings that dictate the conditions under which PowerShell will run scripts. They are not a security boundary against a determined attacker, as they can be easily bypassed by changing the policy or invoking scripts directly. Instead, their purpose is to prevent users from inadvertently running malicious or untrusted scripts.
The policies can be set at different scopes (MachinePolicy, UserPolicy, Process, CurrentUser, LocalMachine), with stricter policies taking precedence. You can check the effective policy using Get-ExecutionPolicy -List.
Common Execution Policies Explained:
- Restricted: (Default for Windows client operating systems) No scripts are allowed to run. PowerShell can only be used in interactive mode. This is the most secure but least functional policy for automation.
- AllSigned: Only scripts signed by a trusted publisher can run. This policy is ideal for highly regulated environments, especially production servers, where every script execution must be traceable and verified. Unsigned scripts, even those written by an administrator, will not run.
- RemoteSigned: (Default for Windows server operating systems) Scripts downloaded from the internet must be signed by a trusted publisher. Local scripts, however, do not need to be signed. This is a common and practical policy for end-user machines, offering a balance between security and usability. It prevents execution of untrusted remote code while allowing internal, local scripts to run without explicit signing.
- Unrestricted: All scripts are allowed to run. For scripts downloaded from the internet, a warning prompt is displayed before execution. This policy significantly reduces security and is generally not recommended for production environments.
- Bypass: Nothing is blocked, and no warnings are displayed. This policy completely disables execution policy checks. It should only be used for very specific, temporary scenarios, such as in automated build pipelines or very controlled test environments where script origin is guaranteed and known. Avoid using this in user-facing or production systems as it removes a critical safety net.
- Undefined: No execution policy is set for the current scope. PowerShell falls back to the policy set in a lower-precedence scope.
To set an execution policy, use the Set-ExecutionPolicy cmdlet. For example, to set RemoteSigned for the current user:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
To set AllSigned for the local machine (requires administrative privileges):
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine
Why Authenticode Signing is Crucial
When an execution policy like AllSigned or RemoteSigned is in effect, PowerShell relies on Authenticode signatures to verify the integrity and origin of scripts. An Authenticode signature embedded in a PowerShell script provides:
- Integrity: It ensures the script has not been tampered with since it was signed. Any modification, even a single character, invalidates the signature.
- Authenticity: It identifies the publisher of the script. If the signing certificate is from a trusted CA, users and systems can be confident about the script's origin.
- Non-repudiation: The publisher cannot deny having signed the script.
For internal scripts, especially those run on production servers with an AllSigned policy, you'll need to sign them with a code-signing certificate. Obtaining these certificates from a public CA can be expensive and cumbersome for internal tools. A more practical approach is to establish an internal Certificate Authority (CA) and issue code-signing certificates from it.
Setting Up an Internal Code-Signing Certificate
Assuming you have an Active Directory Certificate Services (AD CS) CA deployed in your environment:
1. Create a Code Signing Certificate Template
- On your CA server, open the "Certificate Authority" console (
certsrv.msc). - Navigate to Certificate Templates, right-click, and select Manage.
- Find the "Code Signing" template, right-click, and select Duplicate Template.
- On the Compatibility tab, ensure "Certificate Authority" is set to at least "Windows Server 2008 R2" and "Certificate Recipient" to "Windows 7 / Server 2008 R2" or newer.
- On the General tab, give it a meaningful name like "PowerShell Code Signing" and set a desired validity period (e.g., 2 years).
- On the Request Handling tab, ensure "Allow private key to be exported" is checked if you plan to export the PFX for signing on different machines.
- On the Security tab, add the user group or specific user accounts that will be requesting and using this certificate (e.g., your "Script Signers" group or individual administrators). Grant them "Read," "Enroll," and "Autoenroll" permissions.
- Click OK to create the template.
- Back in the "Certificate Authority" console, right-click Certificate Templates -> New -> Certificate Template to Issue. Select your newly created "PowerShell Code Signing" template and click OK.
2. Request and Export the Certificate
- On a workstation or server where you'll sign scripts (ensure it trusts your internal CA), open the "Certificates - Current User" console (
certmgr.mscorcertlm.mscfor local computer). - Navigate to Personal -> Certificates. Right-click, select All Tasks -> Request New Certificate...
- Click Next on the "Before You Begin" page, then Next on "Certificate Enrollment Policy."
- Check the box next to your "PowerShell Code Signing" template. If prompted for more information, provide it.
- Click Enroll, then Finish.
- Locate the newly issued certificate under Personal -> Certificates. It should have "Code Signing" listed under "Intended Purposes."
- Right-click the certificate, select All Tasks -> Export...
- Choose Yes, export the private key.
- Select Personal Information Exchange - PKCS #12 (.PFX). Optionally, check "Include all certificates in the certification path if possible" and "Export all extended properties."
- Set a strong password for the PFX file.
- Choose a save location for the PFX file (e.g.,
C:\Scripts\codesigningcert.pfx).
Note on Certificate Trust: For scripts signed with your internal CA's certificate to be trusted, your CA's root certificate must be deployed to the "Trusted Root Certification Authorities" store on all machines where the scripts will run. This is typically handled automatically by Active Directory Group Policy in domain-joined environments.
Signing Your PowerShell Scripts
Once you have your .pfx certificate file, you can sign your scripts using the Set-AuthenticodeSignature cmdlet.
1. Importing the Certificate (if not already installed)
If you're signing on a machine where the certificate isn't already installed, you'll need to import the PFX file into the user's or machine's certificate store first. For a temporary import to sign scripts, you might load it into a variable:
$certPath = "C:\Scripts\codesigningcert.pfx"
$certPass = ConvertTo-SecureString "YourCertificatePassword" -AsPlainText -Force
$codeSigningCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, $certPass, "Exportable")
# Verify the certificate loaded correctly
Write-Host "Certificate Subject: $($codeSigningCert.Subject)"
Write-Host "Certificate Issuer: $($codeSigningCert.Issuer)"
Write-Host "Certificate Valid From: $($codeSigningCert.NotBefore)"
Write-Host "Certificate Valid To: $($codeSigningCert.NotAfter)"
Alternatively, import it permanently into the current user store via Import-PfxCertificate or certmgr.msc.
2. Signing the Script
With the certificate available (either loaded into a variable or installed in a certificate store accessible by the signing user), you can sign your script:
# Assuming $codeSigningCert holds your certificate object
# OR, if installed in the store:
# $codeSigningCert = Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.Subject -like "*CN=Your Script Signing Cert Name*"} | Select-Object -First 1
$scriptPath = "C:\Scripts\MyAwesomeScript.ps1"
# Sign the script
Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $codeSigningCert -TimestampServer "http://timestamp.digicert.com"
# Verify the signature
Get-AuthenticodeSignature -FilePath $scriptPath | Format-List *
The -TimestampServer parameter is highly recommended. It adds a timestamp to your signature, ensuring the script remains valid even after your code-signing certificate expires, as long as it was valid at the time of signing. Public timestamp servers like http://timestamp.digicert.com or http://timestamp.sectigo.com are commonly used.
When you view the script file in a text editor after signing, you'll see a comment block appended to the end, containing the signature information:
# SIG # Begin signature information
# MIIi2wYJKoZIhvcNAQcCoIIi0wYJKoZIhvcNAQcDMQ...
# SIG # End signature information
Do not edit the script after signing! Any change, even a space, will invalidate the signature. If you need to update the script, edit it, then re-sign it.
Deploying Signed Scripts
Once scripts are signed with a trusted certificate, they can be deployed to systems running RemoteSigned or AllSigned policies. Ensure the entire certificate chain (root and any intermediate CAs) is trusted on the target systems. Group Policy is the primary method for distributing trusted root certificates in a domain environment.
Common Pitfalls and Troubleshooting
- Untrusted Root CA: If scripts signed by your internal CA aren't running, verify that your internal CA's root certificate is installed in the "Trusted Root Certification Authorities" store on the target machine. Use
certmgr.mscto check. - Expired Certificate: Ensure your code-signing certificate is still valid. If it has expired, new scripts cannot be signed, and scripts signed without a timestamp will no longer be trusted.
- Modified Script: Any modification to a signed script invalidates its signature. Always re-sign after changes.
- Incorrect Policy Scope: An execution policy set at a higher precedence scope (e.g.,
MachinePolicyvia GPO) will override a policy set at a lower scope (e.g.,CurrentUser). UseGet-ExecutionPolicy -Listto see the effective policy for all scopes. - Bypass Policy Abuse: Avoid using
Bypassunless absolutely necessary for very specific, isolated automation. It completely negates the safety benefits of execution policies. If a script fails due to policy, identify the real issue (e.g., untrusted certificate, unsigned script) rather than just bypassing the check. - Using the wrong certificate: Ensure the certificate you're using for signing has "Code Signing" listed under its "Intended Purposes." Using a certificate meant for server authentication or encryption will not work.
- Timestamp Server Issues: If signing fails with a network error, check connectivity to the timestamp server. If you don't use a timestamp server, scripts will fail to run once the signing certificate expires, even if the script was signed when the certificate was valid.