PowerShell security: logging, v2 removal, CLM and JEA
Make PowerShell transparent and bounded: script block, module and transcription logging, removing PowerShell 2.0, Constrained Language Mode enforced by App Control, and Just Enough Administration.
PowerShell is the administration layer of Windows and a favourite tool for attackers, because it is present everywhere, runs code from memory and can reach almost every API. Trying to block it usually breaks management tools while motivated attackers find other hosts for the engine. The durable approach is different: make every PowerShell action visible and restrict the language available to untrusted code.
1. Script block logging
Script block logging records the content of every script block PowerShell processes, after de-obfuscation, which is what defeats most encoded and obfuscated payloads.
- GPO: Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging = Enabled. Leave Log script block invocation start / stop events unchecked (very noisy).
- Registry:
HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLoggingEnableScriptBlockLogging=1. - Events: 4104 in Applications and Services Logs > Microsoft > Windows > PowerShell > Operational. Long scripts are split across several 4104 events linked by a script block ID.
Even without this policy, PowerShell 5 automatically logs script blocks it considers suspicious as warning-level 4104 events. The policy makes logging complete.
2. Module logging
Module logging records pipeline execution details, including parameter values, for the modules you choose.
- GPO: Turn on Module Logging = Enabled, module names
*(all modules). - Events: 4103 in the same log.
It overlaps with script block logging but captures runtime values (such as variables expanded into a command) that the script text alone does not show.
3. Transcription
Transcription writes a text record of each session's input and output to files, handy for reviewing interactive admin activity.
- GPO: Turn on PowerShell Transcription = Enabled, output directory a write-only network share (for example
\\logsrv\pstranscripts$), Include invocation headers checked. - Share permissions: computers and users can create files and write, but not read or list other people's transcripts; only the security team can read.
Treat transcripts and script block logs as sensitive: admins sometimes paste passwords into consoles. If that risk is significant, Protected Event Logging (Administrative Templates > Windows Components > Event Logging > Enable Protected Event Logging) encrypts event log content with a certificate so only your SIEM (with the private key) can read it.
4. Size and forward the logs
The default PowerShell Operational log is small and rolls over within hours on a busy server. Increase it and forward it:
wevtutil sl "Microsoft-Windows-PowerShell/Operational" /ms:268435456 # 256 MBForward 4103 and 4104 (and 400/403 from the classic Windows PowerShell log, which record engine start and stop with the host version) via Windows Event Forwarding or your agent. See the audit policy and Sysmon guide.
5. Remove PowerShell 2.0
The PowerShell 2.0 engine predates script block logging, AMSI and Constrained Language Mode. If it is installed, an attacker can run powershell.exe -Version 2 and bypass all of the above. It also depends on .NET Framework 3.5, which is often present.
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart
# Windows Server
Uninstall-WindowsFeature PowerShell-V2Microsoft removed PowerShell 2.0 from Windows 11 24H2 starting with the August 2025 updates and from Windows Server 2025 starting in September 2025; older releases and images still carry it. Detect downgrade attempts via event 400 in the Windows PowerShell log where EngineVersion is 2.0.
6. AMSI
The Antimalware Scan Interface lets PowerShell (and VBScript, JScript, Office VBA and others) hand script content to the antivirus before execution, again after de-obfuscation. It is on by default with Microsoft Defender or any AMSI-aware antivirus. Your job is to keep it that way:
- Keep Defender platform and engine updates current.
- Alert on detections mentioning AMSI bypass attempts and on PowerShell events containing well-known bypass patterns (for example references to
AmsiUtilsoramsiInitFailed).
7. Constrained Language Mode, enforced properly
Constrained Language Mode (CLM) removes the parts of the language attackers depend on: arbitrary .NET types, Add-Type, COM objects outside an allowlist, and direct Win32 API calls. Core cmdlets still work, so most admin scripts that just manage Windows run fine.
Check the current mode:
$ExecutionContext.SessionState.LanguageModeThe only robust way to enforce CLM is application control. When an App Control for Business (WDAC) or AppLocker script policy is enforced, PowerShell automatically:
- runs interactive sessions and scripts not allowed by the policy in Constrained Language Mode;
- runs scripts allowed by the policy (for example signed by your internal code-signing certificate) in Full Language Mode.
Setting the environment variable __PSLockdownPolicy to 4 is sometimes suggested as a shortcut. It is not a security boundary: a user can remove it. Use it only in a lab to test script compatibility.
Sign your admin scripts with an internal code-signing certificate so they keep full language under the policy:
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
Set-AuthenticodeSignature -FilePath .\Invoke-Maintenance.ps1 -Certificate $cert `
-TimestampServer "http://timestamp.digicert.com"See App Control vs AppLocker for building the policy itself.
8. Just Enough Administration (JEA)
Many helpdesk and operator tasks don't need a full admin session. JEA exposes a PowerShell remoting endpoint where users can only run specific commands, as a temporary virtual account with the necessary rights, and every command is transcribed.
A minimal example: allow operators to restart one service and read its status on a server.
# Role capability: what the role can do
New-PSRoleCapabilityFile -Path "C:\Program Files\WindowsPowerShell\Modules\OpsJEA\RoleCapabilities\PrintOps.psrc" `
-VisibleCmdlets @{ Name = 'Restart-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler' } },
'Get-Service'
# Session configuration: who gets which role, run as a virtual account
New-PSSessionConfigurationFile -Path .\PrintOps.pssc -SessionType RestrictedRemoteServer `
-RunAsVirtualAccount -TranscriptDirectory "C:\ProgramData\JEA\Transcripts" `
-RoleDefinitions @{ 'CORP\Print-Operators' = @{ RoleCapabilities = 'PrintOps' } }
Register-PSSessionConfiguration -Name PrintOps -Path .\PrintOps.psscUsers connect with Enter-PSSession -ComputerName srv01 -ConfigurationName PrintOps and get exactly those two commands. Be careful which cmdlets you expose: anything that can run arbitrary code (Invoke-Expression, Start-Process without constraints, Add-Type) or write files to arbitrary paths turns the endpoint back into full admin.
9. WinRM itself
- Keep WinRM listening only where it is needed and firewall it to management hosts (firewall guide).
- Disallow Basic authentication and unencrypted traffic (Windows Components > Windows Remote Management (WinRM) > WinRM Service / Client: Allow Basic authentication = Disabled, Allow unencrypted traffic = Disabled). These are the defaults; the baselines enforce them so they can't drift.
- Disable Allow CredSSP authentication unless a specific workflow needs it; CredSSP sends reusable credentials to the remote host.
Checklist
- Script block logging (4104) and module logging (4103) on everywhere
- Transcription to a write-only share, or Protected Event Logging where sensitive
- PowerShell Operational log at 256 MB or more, forwarded
- PowerShell 2.0 removed; event 400 with EngineVersion 2.0 alerted
- CLM enforced via App Control / AppLocker; admin scripts signed
- JEA endpoints for delegated operational tasks
- WinRM: no Basic, no unencrypted, no CredSSP, firewall-scoped
- PowerShell 7 policies configured if PowerShell 7 is installed
With this in place, PowerShell stays fully useful to administrators, while an attacker's PowerShell is logged in clear text and stripped of the features their tooling needs.
Frequently asked questions
Should I block powershell.exe?
Usually not. Blocking the executable breaks administration and management agents, and attackers can host the PowerShell engine in other processes. Logging it thoroughly and enforcing Constrained Language Mode through application control is far more effective.
Is the execution policy a security control?
No. Microsoft documents the execution policy as a safety feature to prevent accidental script execution, not a security boundary. It can be bypassed trivially, for example with -ExecutionPolicy Bypass. Use it, but don't rely on it.
Do these settings apply to PowerShell 7?
PowerShell 7 has its own policy settings (under the PowerShell Core administrative templates shipped with PowerShell 7, or a powershell.config.json). It can be configured to use the Windows PowerShell policy settings. Configure both if PowerShell 7 is deployed.
PowerShell security: logging, v2 removal, CLM and JEA