How to Configure PowerShell Execution Policies on Windows Server 2012 R2

PowerShell execution policies control which scripts are allowed to run on a Windows system. On Windows Server 2012 R2, the default execution policy for Server Core installations is RemoteSigned, which permits locally written scripts and requires digital signatures only for scripts downloaded from the internet. Desktop Experience installations typically default to Restricted, which blocks all script execution. Understanding execution policies — their scope hierarchy, practical implications, how to configure them, and how to sign scripts — is fundamental to deploying PowerShell automation securely in a production server environment.

Execution policies are not a security boundary in the traditional sense; a determined user with local administrator rights can always bypass them. Their primary purpose is to prevent accidental execution of scripts and to serve as a policy expression about the organization’s expectations for script authorization. Pair execution policies with AppLocker or Software Restriction Policies if you need a true security control over script execution.

Prerequisites

– Windows Server 2012 R2 with PowerShell 4.0
– Administrative credentials for machine-level policy changes
– Active Directory domain membership for Group Policy distribution
– A code-signing certificate if you will be signing scripts

Step 1: Understand Execution Policy Scope and Values

# View the effective execution policy and all scopes
Get-ExecutionPolicy -List

# Output shows all scopes in precedence order (highest first):
# MachinePolicy  - Set by Group Policy, applies to all users on the machine
# UserPolicy     - Set by Group Policy, applies to the current user
# Process        - Current process only, overrides LocalMachine/CurrentUser
# CurrentUser    - Applies to current user, stored in HKCU registry
# LocalMachine   - Applies to all users on the machine, stored in HKLM registry

# The effective policy is the most restrictive policy that applies at runtime
Get-ExecutionPolicy  # Shows the currently effective policy

# Execution policy values:
# Restricted    - No scripts can run (default on client OS)
# AllSigned     - All scripts must be digitally signed by a trusted publisher
# RemoteSigned  - Local scripts run freely; remote scripts must be signed
# Unrestricted  - All scripts run but remote scripts prompt for confirmation
# Bypass        - Nothing is blocked, no warnings or prompts
# Undefined     - No policy set at this scope (higher scope determines policy)

Step 2: Configure Execution Policy for Server Automation

# Set policy for all users on the local machine
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force

# Set policy for the current user only
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force

# Set policy for the current process only (temporary — lasts only for this session)
Set-ExecutionPolicy Bypass -Scope Process -Force

# Verify effective policy after changes
Get-ExecutionPolicy -List
Write-Host "Effective policy: $(Get-ExecutionPolicy)"

# Recommended settings for Windows Server 2012 R2:
# - LocalMachine: RemoteSigned (allows local scripts, requires signing for internet-sourced scripts)
# - Distribution automation: consider AllSigned for production environments
# - Development: RemoteSigned is practical and reasonable

Step 3: Deploy Execution Policy via Group Policy

# Group Policy path to configure execution policy:
# Computer Configuration > Administrative Templates > Windows Components > 
# Windows PowerShell > Turn on Script Execution

# Settings:
# - "Allow only signed scripts" = AllSigned
# - "Allow local scripts and remote signed scripts" = RemoteSigned
# - "Allow all scripts" = Unrestricted

# Configure via PowerShell using the GroupPolicy module (requires GPMC):
Import-Module GroupPolicy

# Get the GPO (or create one)
$gpo = Get-GPO -Name "Server PowerShell Policy" -ErrorAction SilentlyContinue
if (-not $gpo) {
    $gpo = New-GPO -Name "Server PowerShell Policy"
}

# Set the execution policy via registry preference
Set-GPRegistryValue -Name "Server PowerShell Policy" `
    -Key "HKLMSOFTWAREPoliciesMicrosoftWindowsPowerShell" `
    -ValueName "EnableScripts" `
    -Type DWord -Value 1

Set-GPRegistryValue -Name "Server PowerShell Policy" `
    -Key "HKLMSOFTWAREPoliciesMicrosoftWindowsPowerShell" `
    -ValueName "ExecutionPolicy" `
    -Type String -Value "RemoteSigned"

# Link GPO to an OU
New-GPLink -Name "Server PowerShell Policy" -Target "OU=Servers,DC=domain,DC=com"

Step 4: Unblock Downloaded Scripts

Files downloaded from the internet receive an Alternate Data Stream (ADS) Zone identifier that marks them as “internet zone.” PowerShell treats these as “remote” scripts subject to the execution policy signature requirement:

# Check if a file has the internet zone marker
Get-Item "C:DownloadsMyScript.ps1" -Stream Zone.Identifier -ErrorAction SilentlyContinue

# View the zone ID (3 = Internet zone, 2 = Trusted Sites zone)
Get-Content "C:DownloadsMyScript.ps1:Zone.Identifier"

# Unblock a single file (removes the Zone.Identifier ADS)
Unblock-File -Path "C:DownloadsMyScript.ps1"

# Unblock all PS1 files in a directory after reviewing them
Get-ChildItem "C:DownloadsScripts" -Filter "*.ps1" |
    Where-Object { (Get-Item $_.FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue) } |
    ForEach-Object {
        Write-Host "Unblocking: $($_.Name)"
        Unblock-File -Path $_.FullName
    }

# List all scripts in a directory that are blocked
Get-ChildItem "C:Scripts" -Recurse -Filter "*.ps1" | ForEach-Object {
    $zoneStream = Get-Item $_.FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue
    if ($zoneStream) {
        Write-Host "BLOCKED: $($_.FullName)"
    }
}

Step 5: Sign Scripts with a Code-Signing Certificate

Digital signatures on scripts allow them to run under AllSigned or RemoteSigned policies without user prompts:

# Get an existing code-signing certificate from the personal certificate store
$cert = Get-ChildItem Cert:CurrentUserMy -CodeSigningCert | Select-Object -First 1

if (-not $cert) {
    # Create a self-signed code-signing certificate for testing
    $cert = New-SelfSignedCertificate `
        -Subject "CN=PowerShell Code Signing,O=YourOrg" `
        -CertStoreLocation "Cert:CurrentUserMy" `
        -KeyUsage DigitalSignature `
        -Type CodeSigningCert `
        -NotAfter (Get-Date).AddYears(2)
    
    Write-Host "Created self-signed cert: $($cert.Thumbprint)"
    # Note: Self-signed certs must be added to TrustedPublisher store to be usable under AllSigned
    # Export and add to machine TrustedPublisher if needed for policy compliance
}

# Sign a script
Set-AuthenticodeSignature -FilePath "C:ScriptsDeployWebApp.ps1" -Certificate $cert

# Verify the signature
Get-AuthenticodeSignature "C:ScriptsDeployWebApp.ps1" | Select-Object Path, Status, SignerCertificate

# Sign all PS1 files in a directory
Get-ChildItem "C:Scripts" -Filter "*.ps1" | ForEach-Object {
    Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert
    Write-Host "Signed: $($_.Name)"
}

Step 6: Request a Code-Signing Certificate from Enterprise CA

# Enroll for a code-signing certificate from internal CA via Certificate template
# Requires a "Code Signing" or "Code Signing (Template)" available to your account

# Method 1: Via certreq (if Code Signing template is available)
$infContent = @"
[Version]
Signature = "$Windows NT$"

[NewRequest]
Subject = "CN=$env:USERNAME Code Signing,O=YourOrganization"
KeySpec = AT_SIGNATURE
KeyLength = 2048
SMIME = FALSE
HashAlgorithm = sha256
Exportable = TRUE
MachineKeySet = FALSE
RequestType = PKCS10

[EnhancedKeyUsageExtension]
OID = 1.3.6.1.5.5.7.3.3
"@

$infContent | Out-File "C:tempcodesigning.inf" -Encoding ASCII
certreq -New "C:tempcodesigning.inf" "C:tempcodesigning.req"
# Submit req to CA, then retrieve:
# certreq -Submit -config "CA-ServerCA-Name" "C:tempcodesigning.req" "C:tempcodesigning.cer"
# certreq -Accept "C:tempcodesigning.cer"

# Method 2: Get-Certificate (when template name is known)
Get-Certificate -Template "CodeSigning" `
    -SubjectName "CN=$env:USERNAME Code Signing" `
    -CertStoreLocation Cert:CurrentUserMy

Step 7: Configure PSModulePath for Custom Modules

# View the current PSModulePath
$env:PSModulePath -split ";"

# Default paths on WS2012 R2:
# C:UsersDocumentsWindowsPowerShellModules  (user scope)
# C:Windowssystem32WindowsPowerShellv1.0Modules   (system scope)

# Add a custom module path permanently for all users (machine-level)
$customPath = "C:PowerShellModules"
New-Item -Path $customPath -ItemType Directory -Force

$currentMachinePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine")
if ($currentMachinePath -notlike "*$customPath*") {
    [System.Environment]::SetEnvironmentVariable(
        "PSModulePath",
        "$currentMachinePath;$customPath",
        "Machine"
    )
    Write-Host "Added $customPath to machine PSModulePath"
}

# Reload PSModulePath in current session
$env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine") + ";" +
                    [System.Environment]::GetEnvironmentVariable("PSModulePath","User")

# Place modules in the custom path and they will be autoloaded in PowerShell 4.0+
# Module structure: $customPathModuleNameModuleName.psm1 (and optionally .psd1)

Step 8: Install and Manage PowerShell Modules

# List all available modules (including not-yet-imported)
Get-Module -ListAvailable | Select-Object Name, ModuleType, Version, Path | Sort-Object Name

# Import a module into the current session
Import-Module ServerManager
Import-Module ActiveDirectory
Import-Module DnsServer

# Autoloading: In PowerShell 4.0, calling any cmdlet from a module will auto-import
# the module if it is in PSModulePath — no explicit Import-Module needed

# View imported modules in current session
Get-Module | Select-Object Name, Version, ModuleType, ExportedCmdlets

# Remove a module from the current session
Remove-Module ActiveDirectory

# Find commands from a module without importing it
Get-Command -Module DnsServer

# Discover which module a cmdlet comes from
(Get-Command Get-DnsServerZone).ModuleName

# Manually install a module from file (copy to PSModulePath directory)
Copy-Item -Path "\FileServer01ModulesMyCorpModule" `
    -Destination "C:PowerShellModulesMyCorpModule" -Recurse

# Verify module is available after copy
Get-Module -ListAvailable -Name MyCorpModule

Step 9: Create a Simple PowerShell Script Module

# Create a module directory and .psm1 file
New-Item -Path "C:PowerShellModulesCorpUtils" -ItemType Directory -Force

$moduleContent = @'
# CorpUtils.psm1 - Corporate utility functions for WS2012 R2 administration

function Get-ServerHealth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )
    
    foreach ($computer in $ComputerName) {
        $ping = Test-Connection $computer -Count 1 -Quiet
        $obj = [PSCustomObject]@{
            Computer  = $computer
            Online    = $ping
            OS        = $null
            Uptime    = $null
            FreeDiskC = $null
        }
        
        if ($ping) {
            $os = Get-WmiObject Win32_OperatingSystem -ComputerName $computer -ErrorAction SilentlyContinue
            if ($os) {
                $obj.OS     = $os.Caption
                $obj.Uptime = (Get-Date) - $os.ConvertToDateTime($os.LastBootUpTime)
            }
            $disk = Get-WmiObject Win32_LogicalDisk -ComputerName $computer -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
            if ($disk) {
                $obj.FreeDiskC = "{0:N1} GB" -f ($disk.FreeSpace / 1GB)
            }
        }
        $obj
    }
}

function Test-ServiceRunning {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ServiceName,
        [string]$ComputerName = $env:COMPUTERNAME
    )
    $svc = Get-Service -Name $ServiceName -ComputerName $ComputerName -ErrorAction SilentlyContinue
    if ($svc) {
        return $svc.Status -eq "Running"
    }
    return $false
}

Export-ModuleMember -Function Get-ServerHealth, Test-ServiceRunning
'@

$moduleContent | Out-File "C:PowerShellModulesCorpUtilsCorpUtils.psm1" -Encoding UTF8

# Test the module
Import-Module CorpUtils
Get-Command -Module CorpUtils

Step 10: Create a Module Manifest

# Create a module manifest (.psd1) for versioning and metadata
New-ModuleManifest -Path "C:PowerShellModulesCorpUtilsCorpUtils.psd1" `
    -RootModule "CorpUtils.psm1" `
    -ModuleVersion "1.2.0" `
    -Author "IT Operations Team" `
    -CompanyName "YourOrganization" `
    -Description "Corporate PowerShell utility functions for WS2012 R2 server administration" `
    -PowerShellVersion "4.0" `
    -FunctionsToExport @("Get-ServerHealth","Test-ServiceRunning") `
    -Tags @("Administration","Servers","Health") `
    -ProjectUri "http://intranet/wiki/PowerShellModules"

# View the manifest
Get-Content "C:PowerShellModulesCorpUtilsCorpUtils.psd1"

# Verify the manifest is valid
Test-ModuleManifest "C:PowerShellModulesCorpUtilsCorpUtils.psd1"

# Import the manifest-driven module
Remove-Module CorpUtils -ErrorAction SilentlyContinue
Import-Module CorpUtils
Get-Module CorpUtils | Select-Object Name, Version, Description, ExportedFunctions

Summary

Execution policies on Windows Server 2012 R2 provide a graduated approach to script authorization, ranging from fully permissive Bypass through the balanced RemoteSigned default to the enterprise-grade AllSigned policy that requires every script to be digitally signed. Deploying execution policies via Group Policy ensures consistent, tamper-resistant settings across the server fleet. Pairing a clear execution policy with a custom PSModulePath configured at the machine level creates a controlled module library where IT-approved functions are available organization-wide, while the module manifest system provides version control and metadata that enables clean governance of PowerShell tools in production environments.