Sign scripts and harden ExecutionPolicy with AppLocker and WDAC

Last update: 17/12/2025
Author Isaac
  • The combination of AppLocker/WDAC with ExecutionPolicy and script signing allows granular control of what code is executed on computers.
  • Execution policies are not an absolute security system, but they reduce human error and force the adoption of more secure practices such as AllSigned or RemoteSigned.
  • The use of code signing certificates (ideally from a corporate PKI) is key to validating scripts in strict policies and large-scale deployments.
  • Real security is achieved by adding several layers: AppLocker/WDAC, ExecutionPolicy, permissions NTFSGPO and good certificate and script management.

WDAC and AppLocker configuration

In many business environments, the quick fix is ​​still used: lowering PowerShell 's defenses by setting the ExecutionPolicy to Unrestricted . It works, yes, but it opens the door wide for any user to run potentially dangerous scripts, both on servers and user computers.

The reality is that Windows gives us very powerful tools to do things right: signing scripts, hardening execution policies, and complementing it with AppLocker or WDAC to control what code runs on computers. Properly configured, you can have automation, flexibility, and security simultaneously, without resorting to brute force.

AppLocker and WDAC: controlling which scripts run

AppLocker and its successor, Windows Defender Application Control (WDAC), allow you to define which scripts and binaries are authorized to run on your systems. They don't replace PowerShell's ExecutionPolicy, but rather complement it as an additional layer of defense.

AppLocker works with separate rule sets (executables, installers, scripts, packaged applications, etc.), and in the specific case of scripts, it only controls a few very specific file types. This is important because many organizations believe they "block everything" when in reality they only filter a few formats.

In the script rules collection, AppLocker only supports the following file formats :

  • . Ps1 – PowerShell scripts
  • '.bat' – classic batch files cmd
  • .cmd – another variant of batch scripts
  • .vbs – VBScript scripts
  • . Js – JScript scripts

When you enable AppLocker for scripts, if you don't create any "Allow" rules, the default behavior is very clear: everything is blocked except what the rules explicitly authorize . That's why understanding the default rules is so important.

In a typical policy, AppLocker offers a series of predefined rules for the collection of scripts that are usually used as a basis:

  • Allow local administrators to run all scripts
    Rule (default): “All scripts”
    User: BUILTIN\Administrators
    Route condition: *\ (equivalent to any route)
  • Allow all users to run scripts located in the Windows folder
    Rule (default): “All scripts located in the Windows folder”
    User: Everyone
    Path: %windir%\*
  • Allow all users to run scripts from the Program Files folder
    Rule (default): “All scripts located in the Program Files folder”
    User: Everyone
    Path: %programfiles%\*

With this minimal set, you ensure that the operating system and installed applications function without breaking , but you prevent users from launching scripts from arbitrary locations such as the Desktop or the Downloads folder.

Creating AppLocker rules using GPO

In domain environments, it is common to manage AppLocker through Group Policies , which allows you to apply the same script execution control to hundreds or thousands of computers without having to touch each one.

To configure AppLocker, you work from the Group Policy Manager on a domain controller or an RSAT console. The AppLocker configuration path is:

Path: Configuración del equipo → Directivas → Configuración de Windows → Configuración de seguridad → Directivas de control de aplicaciones → AppLocker

Within AppLocker, you'll find several branches: executables, installers, scripts, and packaged applications. To enforce script usage, focus on the script rules collection and enable rule enforcement by clicking "Configure rule enforcement."

Once enabled, AppLocker behaves like a whitelist: blocking by default and allowing only what you define . To speed up the initial setup, you can use the "Automatically Generate Rules" wizard , which analyzes a folder and creates Allow rules based on:

  • Editor: identifies scripts signed by a specific publisher (very useful for corporate software or software from trusted vendors).
  • Ruta: allows or blocks scripts in specific paths (for example, C:\ScriptsSeguros\*).
  • File hash: it is based on the file's fingerprint, so if the file changes, the rule is no longer valid.

This automatic generation saves a significant amount of time because it creates Allow rules for already installed software , reducing the risk of rendering systems unusable when activating AppLocker. You can then supplement this with manual rules for sensitive paths or corporate scripts.

WDAC: Enhanced version of application control

Windows Defender Application Control (WDAC) is, simply put, a souped-up version of AppLocker for more demanding or modern scenarios . It's designed for environments where you really want a robust whitelisting model for binaries, drivers , and scripts.

  Full Fix: Update Error 0x800f0922 on Windows 10, 8.1, 7

WDAC's logic is similar: only files explicitly permitted by a policy can be executed . This includes EXEs, DLLs, PowerShell scripts, MSIs, drivers, UWP apps, and more. It's much more granular and powerful than AppLocker, but also more complex to deploy.

Many administrators combine WDAC with PowerShell execution policies and script signing to build a layered defense model: the script must be allowed by WDAC/AppLocker and also comply with the ExecutionPolicy . If either fails, it won't run.

PowerShell execution policies: what they are and what they are not

PowerShell's ExecutionPolicy causes a lot of confusion because many people think it's some kind of "impenetrable security wall ." In reality, the official documentation itself makes it clear: it's a lightweight security mechanism designed to prevent human error and accidental executions, not an anti-malware system.

The execution policy, in short, determines the conditions under which PowerShell can load scripts and configuration files . Before running a .ps1 file, PowerShell checks the effective policy and the file's source (local, remote, downloaded from the internet, etc.) and decides whether to allow it, block it, or display warnings.

Some real benefits of implementation policies are:

  • They reduce the likelihood that a user will unintentionally run a malicious script received by email or downloaded from the web.
  • They promote safer scripting practices, such as digital signature with certificates in AllSigned or RemoteSigned policies.
  • They allow different rules to be applied depending on the script's origin (local vs remote), making security more flexible.
  • They can be combined with GPO, AppLocker, and WDAC to create a consistent code control policy.

But we must also be clear about its limitations :

  • They are easy to circumvent for a privileged user, for example by launching powershell.exe -ExecutionPolicy Bypass or using other scripting languages.
  • They do not analyze the script's content: A signed script can be malicious if the certificate has been compromised or the author is malicious.
  • They only affect PowerShell, not VBScript, Pythonnative binaries, etc., so they do not replace a global application control.
  • An excessively lax configuration (“Unrestricted” by default) creates a false sense of security, because it seems that “there is a policy” but in practice it does not block anything relevant.

Types of ExecutionPolicy in PowerShell

PowerShell defines several execution policy modes that determine which scripts can be run and from where . It's important to understand each one well to avoid exceeding or falling short of the limits.

The main types of ExecutionPolicy are:

Restricted

This is the default policy on many Windows clients and means that PowerShell scripts cannot be run at all . Only commands entered interactively in the console or ISE are allowed .

This policy is suitable for workstations or servers where automation is not expected . However, as soon as you need to schedule tasks or deployments using scripts, you will have to change it or use a different policy via Group Policy Objects (GPOs).

RemoteSigned

This is the default policy on many Windows servers. Under this mode, locally created scripts can run without a signature , but any script downloaded from the internet or received from another machine must be signed by a trusted publisher.

This is supported by the mechanism of Mark of the Web (MotW)which labels downloaded files with region information. If you download a .ps1 file from the internet and want to run it unsigned, you'll have to explicitly unlock it with Unblock-File or remove that marking.

AllSigned

Under this policy, all scripts and configuration files, including those created locally, must be digitally signed with a certificate trusted by the system. If any script or file is not signed, or if the signature is invalid, it will not be executed.

In addition, a warning is typically displayed the first time you run a script signed by a new publisher, allowing the user to decide whether or not to trust that certificate. This is the ideal policy for corporate environments with PKI where there is a real need to control who can publish scripts.

Unrestricted

Under this policy , all scripts can be run without mandatory signatures . Remote or downloaded scripts may display a warning to the user before running, but nothing a quick click can't resolve.

It's tempting to use this policy in development environments or labs because "everything works the first time," but in production it's a ticking time bomb. It should be avoided on real workstations and servers , except in very justified cases.

Bypass

In Bypass mode, PowerShell does not apply any restrictions or display any warnings . It's as if there were no ExecutionPolicy, which is primarily intended for automation processes, orchestrators, integrations with other applications, or CI/CD pipelines where another layer of security already exists.

  Assigning multimedia commands to Logitech keyboards with Logi Options+: shortcuts, gestures, and macros

Using Bypass as a global policy on user computers is essentially giving up any kind of protection against scripts . If it is used, it should be in a controlled manner, limited to specific sessions or very specific processes.

Undefined and Default

The Undefined option indicates that there is no explicit policy configured in that scope. If all scopes are set to Undefined, the default behavior will be Restricted on Windows clients and RemoteSigned on servers.

The Default option is usually used to return to the product's default value on that specific system, i.e., Restricted for client and RemoteSigned for server.

ExecutionPolicy Scopes and Priority

The same machine can have multiple execution policies defined simultaneously , each in a different scope. PowerShell determines which one is effective based on a priority hierarchy.

The main scopes are:

  • Machine Policy: It is defined by GPO at the computer level; it applies to all users of the machine.
  • UserPolicy: also via GPO, but it only affects the specific user to whom the policy applies.
  • : It only applies to the current PowerShell session and lives in memory; when you close the console, it disappears.
  • LocalMachine: affects all users of the computer and is normally stored in the registry (HKLM).
  • CurrentUser: applies only to the current user (HKCU).

Precedence between scopes is key, because it explains why sometimes your changes with Set-ExecutionPolicy "do nothing":

  1. MachinePolicy (GPO team)
  2. UserPolicy (GPO user)
  3. LocalMachine
  4. CurrentUser

This means that a policy defined via GPO will always override any local changes you try to make . If MachinePolicy says AllSigned and you try to set it to RemoteSigned in LocalMachine, the system will continue to apply AllSigned.

How to view and change the ExecutionPolicy

To check which policy is actually active, it's advisable to review all policies by scope by running:

Get-ExecutionPolicy -List

This command shows you, from top to bottom, the policies in MachinePolicy, UserPolicy, Process, CurrentUser, and LocalMachine, and which one is effective. It's the basic diagnostic tool when "something doesn't add up."

To change the policy in a specific scope, use Set-ExecutionPolicy . Some typical examples:

  • Set AllSigned at the team level:
    Set-ExecutionPolicy AllSigned -Scope LocalMachine -Force
  • Allow local scripts but require remote scripts to be signed by the current user:
    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
  • Change only the current session (useful for testing):
    Set-ExecutionPolicy Bypass -Scope Process -Force

To modify the policy at the LocalMachine scope, you must open PowerShell as an administrator . If a GPO is defining MachinePolicy or UserPolicy, any attempt to make local changes at lower scopes will return an error indicating that the policy is controlled by a Group Policy Object.

Using GPO to enforce the execution policy

In corporate networks, it's common practice to centralize these configurations using Group Policy Objects (GPOs). This is done using the "Turn on Script Execution" directive , available at:

Path: Configuración del equipo → Plantillas administrativas → Componentes de Windows → Windows PowerShell

This configuration allows :

  • Disable script execution completely (equivalent to Restricted).
  • Enable script execution and choose the policy type (for example, RemoteSigned or AllSigned).
  • Do not configure it, in which case PowerShell will use local policies (Set-ExecutionPolicy) without the GPO interfering.

Once the GPO is linked to the desired OU, you can force its application on clients with gpupdate /force and verify the result with Get-ExecutionPolicy -ListYou'll see how the MachinePolicy or UserPolicy values ​​take control.

Signing PowerShell scripts: the professional approach

Instead of taking the easy way out by lowering security, the reasonable thing for an organization to do is to leave the ExecutionPolicy at AllSigned or RemoteSigned and sign all scripts that will circulate through the domain.

To do this, you need a code signing certificate . There are two options: use an enterprise PKI (such as ADCS) or, for testing purposes, generate a self-signed certificate. In a serious domain environment, it's best to deploy an internal CA and create a specific code signing template.

The typical workflow with ADCS is:

  • Install the Active Directory Certificate Services role on a server.
  • Create or adjust a Code Signing certificate template, defining which users or groups can request it (for example, administrators or DevOps team).
  • Publish the template so that it is available in the CA.
  • From the administrator's computer, open the certificate add-in for the current user (personal store) and request a new code signing certificate to the CA.
  • Distribute the root certificate and, if applicable, intermediate certificates to all computers using GPO, so that the issuer is trusted throughout the domain.

Once you obtain the certificate, you can locate it in the user's store with:

  Everything we know about Windows 12: Features and News

$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert

With that certified document in hand, signing a script is as simple as :

Set-AuthenticodeSignature -FilePath C:\Scripts\MiScript.ps1 -Certificate $cert

If instead of using the local store you have an exported .pfx file (for example, to sign from a build machine), you can load it with:

$cert = Get-PfxCertificate -FilePath C:\Certs\MiFirma.pfx
Set-AuthenticodeSignature -FilePath C:\Scripts\MiScript.ps1 -Certificate $cert

For a corporate environment, it is highly recommended to include the entire certification chain and a timestamping authority , so that the signature remains valid even if the certificate expires:

Set-AuthenticodeSignature -FilePath C:\Scripts\MiScript.ps1 -Certificate $cert -IncludeChain All -TimestampServer "http://timestamp.globalsign.com/scripts/timstamp.dll"

At any time you can check the signing status of a script with:

Get-AuthenticodeSignature .\MiScript.ps1 | Format-List *

If you need to perform quick tests without a real PKI, PowerShell allows you to create a self-signed code-signing certificate :

New-SelfSignedCertificate -FriendlyName "Ejemplo firma de código" -CertStoreLocation Cert:\CurrentUser\My -Subject "CN=FirmaScripts" -Type CodeSigningCert

Bypass and other methods to bypass the ExecutionPolicy

To avoid any misunderstandings, it's important to remember that execution policies can be circumvented in several ways. Therefore, they shouldn't be your only defense, but rather a complement to AppLocker/WDAC, NTFS permissions, and endpoint controls.

The most typical case is that of a system in Restricted or AllSigned mode where, for whatever reason, you need to run a specific unsigned script. If you have sufficient permissions , you can use:

powershell.exe -ExecutionPolicy Bypass -File .\MiScript.ps1

or in short:

powershell -ep Bypass .\MiScript.ps1

This launches a new PowerShell instance just for that execution with the Bypass policy, without touching the global configuration. It's convenient, but also a perfect example of why ExecutionPolicies, on their own, don't stop an attacker with interactive access.

There is also a special environment variable, $env:PSExecutionPolicyPreference , which allows you to temporarily override the policy in the current session:

$env:PSExecutionPolicyPreference = "Bypass"

As long as the variable is set, PowerShell will use that value. You can verify this with:

$env:PSExecutionPolicyPreference

And when you want to return to normal behavior, simply remove the variable:

Remove-Item Env:PSExecutionPolicyPreference

Managing untrusted and blocked scripts

When you download a script from the internet, Windows often marks it with " Mark of the Web ," which causes certain policies (such as RemoteSigned) to treat it as a remote script even though it's on your local disk. The typical symptom is the error message "Script execution is disabled" or "The file is not digitally signed."

If you have reviewed the content and trust it, you can unlock the file without changing the ExecutionPolicy using:

Unblock-File -Path C:\Temp\ScriptDescargado.ps1

Once unlocked , and depending on your policy (for example, RemoteSigned), the script can run if it meets the other requirements. This is a more refined way of working than lowering the policy to Unrestricted.

In environments where AllSigned or RemoteSigned is strictly used, managing trusted certificates for third-party publishers is also important. If you download a script signed by a vendor, you'll need to decide whether to import their certificate as a trusted publisher into the appropriate store, or whether you prefer to re-sign the script with your own internal certificate.

NTFS permissions and Group Policy as reinforcement

In addition to AppLocker, WDAC, and ExecutionPolicy, don't forget the basics: NTFS permissions on the directories where the scripts reside . Even if a user can run a .ps1 file, they shouldn't be able to modify critical scripts that run with high privileges.

Some good practices include:

  • Store production scripts in locations where only administrators or a group of operators have write permissions.
  • Grant end users only read and execute permissions when absolutely necessary.
  • Prevent critical tasks from pulling scripts located in manipulable paths such as Desktop, Downloads, or user profiles.

These restrictions can be reinforced with GPOs that define ACLs on specific folders , as well as with AppLocker policies that only allow script execution from controlled paths and, if possible, signed by your corporate CA.

Combining all of this, you get a scenario where simply having a malicious .ps1 file is not enough : the file must be in an allowed path, comply with the ExecutionPolicy, pass AppLocker/WDAC, and the attacker also needs sufficient permissions to place or modify it.

By working with this layered approach —script signing, hardened ExecutionPolicy, well-thought-out AppLocker/WDAC, correct NTFS permissions, and some user education—it's perfectly feasible to have intensive automation with PowerShell without turning the environment into the Wild West of scripting.