# Powershell Security

> Modern PowerShell security practices for 2025 — SecretManagement, JEA, WDAC, credential protection. PROACTIVELY activate for: (1) Microsoft.PowerShell.SecretManagement and SecretStore for credential storage, (2) Just Enough Administration (JEA) endpoints, (3) Windows Defender Application Control (WDAC) for PowerShell, (4) credential protection (PSCredential, SecureString, DPAPI), (5) module signing and Authenticode signatures, (6) ConstrainedLanguage mode, (7) audit logging (transcription, ScriptBlock logging, module logging), (8) AMSI integration, (9) AppLocker rules for PowerShell, (10) supply-chain security (PSGallery trust, package hash verification). Provides: SecretManagement setup, JEA configuration, WDAC policy templates, signing workflow, and audit-logging configuration.

- Skill: `josiahsiegel/powershell-security` (Agent Skill)
- Install (CLI): `npx skillmds@latest add josiahsiegel/powershell-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/josiahsiegel/powershell-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: josiahsiegel (https://skillmd.com/u/josiahsiegel)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/josiahsiegel/powershell-security

---


# 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

```powershell
# 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

```powershell
# 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

```powershell
# 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

```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
$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

```powershell
# ❌ 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)

```powershell
# 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

```powershell
# 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

```powershell
# 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.

```powershell
# 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

```powershell
# 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

```powershell
# 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.

```powershell
# 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

```powershell
# 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

```powershell
# 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

```powershell
# ❌ 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](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement)
- [JEA Documentation](https://learn.microsoft.com/en-us/powershell/scripting/security/remoting/jea/overview)
- [WDAC Documentation](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control)
- [PowerShell Security Best Practices](https://learn.microsoft.com/en-us/powershell/scripting/security/securing-powershell)
- [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/)

