PowerShell Security Best Practices (2025)
PowerShell安全最佳实践(2025)
Modern security practices for PowerShell scripts and automation, including credential management, SecretManagement module, and hardening techniques.
适用于PowerShell脚本与自动化的现代化安全实践,包括凭证管理、SecretManagement模块及加固技术。
SecretManagement Module (Recommended 2025 Standard)
SecretManagement模块(2025推荐标准)
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.
Microsoft.PowerShell.SecretManagement是PowerShell中用于安全存储凭证的官方解决方案。
为何使用SecretManagement:
- 绝不在脚本中存储明文凭证
- 跨平台的机密存储
- 支持多种密钥库提供商
- 与Azure Key Vault、1Password、KeePass等集成
Install SecretManagement module
Install SecretManagement module
Install-Module -Name Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install-Module -Name Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install vault provider (choose one or more)
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
Install-Module -Name Microsoft.PowerShell.SecretStore # Local encrypted vault
Install-Module -Name Az.KeyVault # Azure Key Vault
Install-Module -Name SecretManagement.KeePass # KeePass integration
Register a vault
Register a vault
Register-SecretVault -Name LocalVault -ModuleName Microsoft.PowerShell.SecretStore
Register-SecretVault -Name LocalVault -ModuleName Microsoft.PowerShell.SecretStore
Store a secret
Store a secret
$password = Read-Host -AsSecureString -Prompt "Enter password"
Set-Secret -Name "DatabasePassword" -Secret $password -Vault LocalVault
$password = Read-Host -AsSecureString -Prompt "Enter password"
Set-Secret -Name "DatabasePassword" -Secret $password -Vault LocalVault
Retrieve a secret
Retrieve a secret
$dbPassword = Get-Secret -Name "DatabasePassword" -Vault LocalVault -AsPlainText
$dbPassword = Get-Secret -Name "DatabasePassword" -Vault LocalVault -AsPlainText
Or as SecureString
Or as SecureString
$dbPasswordSecure = Get-Secret -Name "DatabasePassword" -Vault LocalVault
$dbPasswordSecure = Get-Secret -Name "DatabasePassword" -Vault LocalVault
Remove a secret
Remove a secret
Remove-Secret -Name "DatabasePassword" -Vault LocalVault
Remove-Secret -Name "DatabasePassword" -Vault LocalVault
Azure Key Vault Integration
Azure Key Vault集成
Install and import Az.KeyVault
Install and import Az.KeyVault
Install-Module -Name Az.KeyVault -Scope CurrentUser
Import-Module Az.KeyVault
Install-Module -Name Az.KeyVault -Scope CurrentUser
Import-Module Az.KeyVault
Authenticate to Azure
Authenticate to Azure
Register Azure Key Vault as secret vault
Register Azure Key Vault as secret vault
Register-SecretVault -Name AzureKV
-VaultParameters @{
AZKVaultName = 'MyKeyVault'
SubscriptionId = 'your-subscription-id'
}
Register-SecretVault -Name AzureKV
-VaultParameters @{
AZKVaultName = 'MyKeyVault'
SubscriptionId = 'your-subscription-id'
}
Store secret in Azure Key Vault
Store secret in Azure Key Vault
Set-Secret -Name "ApiKey" -Secret "your-api-key" -Vault AzureKV
Set-Secret -Name "ApiKey" -Secret "your-api-key" -Vault AzureKV
Retrieve from Azure Key Vault
Retrieve from Azure Key Vault
$apiKey = Get-Secret -Name "ApiKey" -Vault AzureKV -AsPlainText
$apiKey = Get-Secret -Name "ApiKey" -Vault AzureKV -AsPlainText
Automation Scripts with SecretManagement
结合SecretManagement的自动化脚本
powershell
<#
.SYNOPSIS
Secure automation script using SecretManagement
.DESCRIPTION
Demonstrates secure credential handling without hardcoded secrets
#>
#Requires -Modules Microsoft.PowerShell.SecretManagement
[CmdletBinding()]
param()
powershell
<#
.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
Retrieve credentials from vault
$dbConnectionString = Get-Secret -Name "SQLConnectionString" -AsPlainText
$apiToken = Get-Secret -Name "APIToken" -AsPlainText
$dbConnectionString = Get-Secret -Name "SQLConnectionString" -AsPlainText
$apiToken = Get-Secret -Name "APIToken" -AsPlainText
Use credentials securely
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() }
}
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
❌ WRONG - Hardcoded credentials
$password = "MyPassword123"
$username = "admin"
$password = "MyPassword123"
$username = "admin"
❌ WRONG - Plaintext in script
❌ WRONG - Plaintext in script
$cred = New-Object System.Management.Automation.PSCredential("admin", "password")
$cred = New-Object System.Management.Automation.PSCredential("admin", "password")
✅ CORRECT - SecretManagement
✅ CORRECT - SecretManagement
$password = Get-Secret -Name "AdminPassword" -AsPlainText
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("admin", $securePassword)
$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)
✅ CORRECT - Interactive prompt (for manual runs)
$cred = Get-Credential -Message "Enter admin credentials"
$cred = Get-Credential -Message "Enter admin credentials"
✅ CORRECT - Managed Identity (Azure automation)
✅ CORRECT - Managed Identity (Azure automation)
Connect-AzAccount -Identity
Connect-AzAccount -Identity
Service Principal Authentication (Azure)
服务主体认证(Azure)
Store service principal credentials in vault
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"
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
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
$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)
Just Enough Administration (JEA)
Just Enough Administration restricts PowerShell remoting sessions to specific cmdlets and parameters.
Just Enough Administration(最小权限管理)可限制PowerShell远程会话仅能使用特定cmdlet和参数。
- Delegate admin tasks without full admin rights
- Compliance requirements (SOC 2, HIPAA, PCI-DSS)
- Production environment hardening
- Audit trail for privileged operations
- 在不授予完整管理员权限的前提下委派管理任务
- 满足合规要求(SOC 2、HIPAA、PCI-DSS)
- 生产环境加固
- 特权操作的审计追踪
Creating a JEA Endpoint
创建JEA端点
1. Create role capability file
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'
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
2. Create session configuration file
New-PSSessionConfigurationFile -Path "C:\JEA\RestartServices.pssc"
-SessionType RestrictedRemoteServer
-RoleDefinitions @{
'DOMAIN\ServiceAdmins' = @{ RoleCapabilities = 'RestartServices' }
} `
-LanguageMode NoLanguage
New-PSSessionConfigurationFile -Path "C:\JEA\RestartServices.pssc"
-SessionType RestrictedRemoteServer
-RoleDefinitions @{
'DOMAIN\ServiceAdmins' = @{ RoleCapabilities = 'RestartServices' }
} `
-LanguageMode NoLanguage
3. Register JEA endpoint
3. Register JEA endpoint
Register-PSSessionConfiguration -Name RestartServices
-Path "C:\JEA\RestartServices.pssc"
-Force
Register-PSSessionConfiguration -Name RestartServices
-Path "C:\JEA\RestartServices.pssc"
-Force
4. Connect to JEA endpoint (as delegated user)
4. Connect to JEA endpoint (as delegated user)
Enter-PSSession -ComputerName Server01 -ConfigurationName RestartServices
Enter-PSSession -ComputerName Server01 -ConfigurationName RestartServices
User can ONLY run allowed commands
User can ONLY run allowed commands
Restart-Service -Name Spooler # ✅ Allowed
Restart-Service -Name DNS # ❌ Denied (not in ValidateSet)
Get-Process # ❌ Denied (not visible)
Restart-Service -Name Spooler # ✅ Allowed
Restart-Service -Name DNS # ❌ Denied (not in ValidateSet)
Get-Process # ❌ Denied (not visible)
Enable transcription and logging
Enable transcription and logging
New-PSSessionConfigurationFile -Path "C:\JEA\AuditedSession.pssc"
-SessionType RestrictedRemoteServer
-TranscriptDirectory "C:\JEA\Transcripts" `
-RunAsVirtualAccount
New-PSSessionConfigurationFile -Path "C:\JEA\AuditedSession.pssc"
-SessionType RestrictedRemoteServer
-TranscriptDirectory "C:\JEA\Transcripts" `
-RunAsVirtualAccount
All JEA sessions are transcribed to C:\JEA\Transcripts
All JEA sessions are transcribed to C:\JEA\Transcripts
Review audit logs
Review audit logs
Get-ChildItem "C:\JEA\Transcripts" | Get-Content
Get-ChildItem "C:\JEA\Transcripts" | Get-Content
Windows Defender Application Control (WDAC)
Windows Defender Application Control (WDAC)
PowerShell Script Control
PowerShell脚本管控
WDAC replaces AppLocker for controlling which PowerShell scripts can execute.
WDAC取代AppLocker,用于管控可执行的PowerShell脚本。
Create WDAC policy for signed scripts only
Create WDAC policy for signed scripts only
New-CIPolicy -FilePath "C:\WDAC\PowerShellPolicy.xml"
-Level FilePublisher
-UserPEs
New-CIPolicy -FilePath "C:\WDAC\PowerShellPolicy.xml"
-Level FilePublisher
-UserPEs
Allow only signed scripts
Allow only signed scripts
Set-RuleOption -FilePath "C:\WDAC\PowerShellPolicy.xml" `
-Option 3 # Required WHQL
Set-RuleOption -FilePath "C:\WDAC\PowerShellPolicy.xml" `
-Option 3 # Required WHQL
Convert to binary policy
Convert to binary policy
ConvertFrom-CIPolicy -XmlFilePath "C:\WDAC\PowerShellPolicy.xml" `
-BinaryFilePath "C:\Windows\System32\CodeIntegrity\SIPolicy.p7b"
ConvertFrom-CIPolicy -XmlFilePath "C:\WDAC\PowerShellPolicy.xml" `
-BinaryFilePath "C:\Windows\System32\CodeIntegrity\SIPolicy.p7b"
Reboot to apply policy
Reboot to apply policy
Why Sign Scripts?
为何要签名脚本?
- Verify script integrity
- Meet organizational security policies
- Enable WDAC enforcement
- Prevent tampering
- 验证脚本完整性
- 满足组织安全策略
- 启用WDAC强制管控
- 防止篡改
Get code signing certificate
Get code signing certificate
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
Set-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1" -Certificate $cert
Set-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1" -Certificate $cert
Verify signature
Verify signature
$signature = Get-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1"
$signature.Status # Should be "Valid"
$signature = Get-AuthenticodeSignature -FilePath "C:\Scripts\MyScript.ps1"
$signature.Status # Should be "Valid"
Check current execution policy
Check current execution policy
Set execution policy (requires admin)
Set execution policy (requires admin)
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
Bypass for single script (testing only)
Bypass for single script (testing only)
PowerShell.exe -ExecutionPolicy Bypass -File "script.ps1"
PowerShell.exe -ExecutionPolicy Bypass -File "script.ps1"
Constrained Language Mode
受限语言模式
What is Constrained Language Mode?
什么是受限语言模式?
Restricts PowerShell language features to prevent malicious code execution.
限制PowerShell语言特性,防止恶意代码执行。
Check current language mode
Check current language mode
$ExecutionContext.SessionState.LanguageMode
$ExecutionContext.SessionState.LanguageMode
Output: FullLanguage (admin) or ConstrainedLanguage (standard user)
Output: FullLanguage (admin) or ConstrainedLanguage (standard user)
Set system-wide constrained language mode
Set system-wide constrained language mode
Via Environment Variable or Group Policy
Via Environment Variable or Group Policy
Set: __PSLockdownPolicy = 4
Set: __PSLockdownPolicy = 4
Test constrained mode behavior
Test constrained mode behavior
FullLanguage allows:
FullLanguage allows:
[System.Net.WebClient]::new() # ✅ Allowed
[System.Net.WebClient]::new() # ✅ Allowed
ConstrainedLanguage blocks:
ConstrainedLanguage blocks:
[System.Net.WebClient]::new() # ❌ Blocked
Add-Type -TypeDefinition "..." # ❌ Blocked
[System.Net.WebClient]::new() # ❌ Blocked
Add-Type -TypeDefinition "..." # ❌ Blocked
Script Block Logging
脚本块日志
Enable via Group Policy or Registry
Enable via Group Policy or Registry
HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
New-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-Name "EnableScriptBlockLogging" -Value 1 -PropertyType DWord
New-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-Name "EnableScriptBlockLogging" -Value 1 -PropertyType DWord
Log location: Windows Event Log
Log location: Windows Event Log
Event Viewer > Applications and Services Logs > Microsoft > Windows > PowerShell > Operational
Event Viewer > Applications and Services Logs > Microsoft > Windows > PowerShell > Operational
Query script block 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
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" |
Where-Object { $_.Id -eq 4104 } | # Script Block Logging event
Select-Object TimeCreated, Message |
Out-GridView
Prevent Injection Attacks
防止注入攻击
❌ WRONG - No validation
❌ WRONG - No validation
function Get-UserData {
param($Username)
Invoke-Sqlcmd -Query "SELECT * FROM Users WHERE Username = '$Username'"
}
function Get-UserData {
param($Username)
Invoke-Sqlcmd -Query "SELECT * FROM Users WHERE Username = '$Username'"
}
Vulnerable to SQL injection
Vulnerable to SQL injection
✅ CORRECT - Parameterized queries
✅ 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}
}
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
✅ CORRECT - ValidateSet for known values
function Restart-AppService {
param(
[ValidateSet('Web', 'API', 'Worker')]
[string]$ServiceName
)
Restart-Service -Name "App${ServiceName}Service"
}
function Restart-AppService {
param(
[ValidateSet('Web', 'API', 'Worker')]
[string]$ServiceName
)
Restart-Service -Name "App${ServiceName}Service"
}
Production Environments
生产环境