PowerShell Security Best Practices (2025)
Modern security practices for PowerShell scripts and automation, including credential management, SecretManagement module, and hardening techniques.
SecretManagement Module (Recommended 2025 Standard)
Overview
Microsoft.PowerShell.SecretManagement is the official solution for secure credential storage in PowerShell.
Why use SecretManagement:
-
Never store plaintext credentials in scripts
-
Cross-platform secret storage
-
Multiple vault provider support
-
Integration with Azure Key Vault, 1Password, KeePass, etc.
Installation
Install SecretManagement module
Install-Module -Name Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install vault provider (choose one or more)
Install-Module -Name Microsoft.PowerShell.SecretStore # Local encrypted vault Install-Module -Name Az.KeyVault # Azure Key Vault Install-Module -Name SecretManagement.KeePass # KeePass integration
Basic Usage
Register a vault
Register-SecretVault -Name LocalVault -ModuleName Microsoft.PowerShell.SecretStore
Store a secret
$password = Read-Host -AsSecureString -Prompt "Enter password" Set-Secret -Name "DatabasePassword" -Secret $password -Vault LocalVault
Retrieve a secret
$dbPassword = Get-Secret -Name "DatabasePassword" -Vault LocalVault -AsPlainText
Or as SecureString
$dbPasswordSecure = Get-Secret -Name "DatabasePassword" -Vault LocalVault
List secrets
Get-SecretInfo
Remove a secret
Remove-Secret -Name "DatabasePassword" -Vault LocalVault
Azure Key Vault Integration
Install and import Az.KeyVault
Install-Module -Name Az.KeyVault -Scope CurrentUser Import-Module Az.KeyVault
Authenticate to Azure
Connect-AzAccount
Register Azure Key Vault as secret vault
Register-SecretVault -Name AzureKV -ModuleName Az.KeyVault
-VaultParameters @{
AZKVaultName = 'MyKeyVault'
SubscriptionId = 'your-subscription-id'
}
Store secret in Azure Key Vault
Set-Secret -Name "ApiKey" -Secret "your-api-key" -Vault AzureKV
Retrieve from Azure Key Vault
$apiKey = Get-Secret -Name "ApiKey" -Vault AzureKV -AsPlainText
Automation Scripts with SecretManagement
<# .SYNOPSIS Secure automation script using SecretManagement
.DESCRIPTION Demonstrates secure credential handling without hardcoded secrets #>
#Requires -Modules Microsoft.PowerShell.SecretManagement
[CmdletBinding()] param()
Retrieve credentials from vault
$dbConnectionString = Get-Secret -Name "SQLConnectionString" -AsPlainText $apiToken = Get-Secret -Name "APIToken" -AsPlainText
Use credentials securely
try { # Database operation $connection = New-Object System.Data.SqlClient.SqlConnection($dbConnectionString) $connection.Open()
# API call with token
$headers = @{ Authorization = "Bearer $apiToken" }
$response = Invoke-RestMethod -Uri "https://api.example.com/data" -Headers $headers
# Process results
Write-Host "Operation completed successfully"
} catch { Write-Error "Operation failed: $_" } finally { if ($connection) { $connection.Close() } }
Credential Management Best Practices
Never Hardcode Credentials
❌ WRONG - Hardcoded credentials
$password = "MyPassword123" $username = "admin"
❌ WRONG - Plaintext in script
$cred = New-Object System.Management.Automation.PSCredential("admin", "password")
✅ CORRECT - SecretManagement
$password = Get-Secret -Name "AdminPassword" -AsPlainText $securePassword = ConvertTo-SecureString $password -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential("admin", $securePassword)
✅ CORRECT - Interactive prompt (for manual runs)
$cred = Get-Credential -Message "Enter admin credentials"
✅ CORRECT - Managed Identity (Azure automation)
Connect-AzAccount -Identity
Service Principal Authentication (Azure)
Store service principal credentials in vault
Set-Secret -Name "AzureAppId" -Secret "app-id-guid" Set-Secret -Name "AzureAppSecret" -Secret "app-secret-value" Set-Secret -Name "AzureTenantId" -Secret "tenant-id-guid"
Retrieve and authenticate
$appId = Get-Secret -Name "AzureAppId" -AsPlainText $appSecret = Get-Secret -Name "AzureAppSecret" -AsPlainText $tenantId = Get-Secret -Name "AzureTenantId" -AsPlainText
$secureSecret = ConvertTo-SecureString $appSecret -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential($appId, $secureSecret)
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant $tenantId
Just Enough Administration (JEA)
What is JEA?
Just Enough Administration restricts PowerShell remoting sessions to specific cmdlets and parameters.
Use Cases
-
Delegate admin tasks without full admin rights
-
Compliance requirements (SOC 2, HIPAA, PCI-DSS)
-
Production environment hardening
-
Audit trail for privileged operations
Creating a JEA Endpoint
1. Create role capability file
New-PSRoleCapabilityFile -Path "C:\JEA\RestartServices.psrc" ` -VisibleCmdlets @{ Name = 'Restart-Service' Parameters = @{ Name = 'Name' ValidateSet = 'Spooler', 'W32Time', 'WinRM' } }, 'Get-Service'
2. Create session configuration file
New-PSSessionConfigurationFile -Path "C:\JEA\RestartServices.pssc" -SessionType RestrictedRemoteServer
-RoleDefinitions @{
'DOMAIN\ServiceAdmins' = @{ RoleCapabilities = 'RestartServices' }
} `
-LanguageMode NoLanguage
3. Register JEA endpoint
Register-PSSessionConfiguration -Name RestartServices -Path "C:\JEA\RestartServices.pssc"
-Force
4. Connect to JEA endpoint (as delegated user)
Enter-PSSession -ComputerName Server01 -ConfigurationName RestartServices
User can ONLY run allowed commands
Restart-Service -Name Spooler # ✅ Allowed Restart-Service -Name DNS # ❌ Denied (not in ValidateSet) Get-Process # ❌ Denied (not visible)
JEA Audit Logging
Enable transcription and logging
New-PSSessionConfigurationFile -Path "C:\JEA\AuditedSession.pssc" -SessionType RestrictedRemoteServer
-TranscriptDirectory "C:\JEA\Transcripts" `
-RunAsVirtualAccount
All JEA sessions are transcribed to C:\JEA\Transcripts
Review audit logs
Get-ChildItem "C:\JEA\Transcripts" | Get-Content
Windows Defender Application Control (WDAC)
PowerShell Script Control
WDAC replaces AppLocker for controlling which PowerShell scripts can execute.
Create WDAC policy for signed scripts only
New-CIPolicy -FilePath "C:\WDAC\PowerShellPolicy.xml" -ScanPath "C:\Scripts"
-Level FilePublisher -Fallback Hash
-UserPEs
Allow only signed scripts
Set-RuleOption -FilePath "C:\WDAC\PowerShellPolicy.xml" ` -Option 3 # Required WHQL
Convert to binary policy
ConvertFrom-CIPolicy -XmlFilePath "C:\WDAC\PowerShellPolicy.xml" ` -BinaryFilePath "C:\Windows\System32\CodeIntegrity\SIPolicy.p7b"
Reboot to apply policy
Restart-Computer
Code Signing
Why Sign Scripts?
-
Verify script integrity
-
Meet organizational security policies
-
Enable WDAC enforcement
-
Prevent tampering
Signing a Script
Get code signing certificate
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
Sign script
Set-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1" -Certificate $cert
Verify signature
$signature = Get-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1" $signature.Status # Should be "Valid"
Execution Policy
Check current execution policy
Get-ExecutionPolicy
Set execution policy (requires admin)
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
Bypass for single script (testing only)
PowerShell.exe -ExecutionPolicy Bypass -File "script.ps1"
Constrained Language Mode
What is Constrained Language Mode?
Restricts PowerShell language features to prevent malicious code execution.
Check current language mode
$ExecutionContext.SessionState.LanguageMode
Output: FullLanguage (admin) or ConstrainedLanguage (standard user)
Set system-wide constrained language mode
Via Environment Variable or Group Policy
Set: __PSLockdownPolicy = 4
Test constrained mode behavior
FullLanguage allows:
[System.Net.WebClient]::new() # ✅ Allowed
ConstrainedLanguage blocks:
[System.Net.WebClient]::new() # ❌ Blocked Add-Type -TypeDefinition "..." # ❌ Blocked
Script Block Logging
Enable Logging
Enable via Group Policy or Registry
HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
New-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" ` -Name "EnableScriptBlockLogging" -Value 1 -PropertyType DWord
Log location: Windows Event Log
Event Viewer > Applications and Services Logs > Microsoft > Windows > PowerShell > Operational
Review Logs
Query script block logs
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object { $_.Id -eq 4104 } | # Script Block Logging event Select-Object TimeCreated, Message | Out-GridView
Input Validation
Prevent Injection Attacks
❌ WRONG - No validation
function Get-UserData { param($Username) Invoke-Sqlcmd -Query "SELECT * FROM Users WHERE Username = '$Username'" }
Vulnerable to SQL injection
✅ CORRECT - Parameterized queries
function Get-UserData { param( [ValidatePattern('^[a-zA-Z0-9_-]+$')] [string]$Username ) Invoke-Sqlcmd -Query "SELECT * FROM Users WHERE Username = @Username" ` -Variable @{Username=$Username} }
✅ CORRECT - ValidateSet for known values
function Restart-AppService { param( [ValidateSet('Web', 'API', 'Worker')] [string]$ServiceName ) Restart-Service -Name "App${ServiceName}Service" }
Security Checklist
Script Development
-
Never hardcode credentials (use SecretManagement)
-
Use parameterized queries for SQL operations
-
Validate all user input with [ValidatePattern] , [ValidateSet] , etc.
-
Enable Set-StrictMode -Version Latest
-
Use try/catch for error handling
-
Avoid Invoke-Expression with user input
-
Sign production scripts
-
Enable Script Block Logging
Automation
-
Use Managed Identity or Service Principal (never passwords)
-
Store secrets in SecretManagement or Azure Key Vault
-
Implement JEA for delegated admin tasks
-
Enable audit logging for all privileged operations
-
Use least privilege principle
-
Rotate credentials regularly
-
Monitor failed authentication attempts
Production Environments
-
Implement WDAC policies for script control
-
Use Constrained Language Mode for non-admin users
-
Enable PowerShell logging (Script Block + Transcription)
-
Require signed scripts (via execution policy)
-
Regular security audits
-
Keep PowerShell updated (7.5+)
-
Use JEA for remote administration
Resources
-
SecretManagement Documentation
-
JEA Documentation
-
WDAC Documentation
-
PowerShell Security Best Practices
-
Azure Key Vault