Skip to content

Credential Management

Note

Handle passwords and credentials safely in PowerShell using SecureString, PSCredential, Get-Credential, and encrypted export/import — never plaintext.

Overview

Every script that needs a password eventually has to answer: where does this password live, and who can read it? PowerShell's credential objects — SecureString and PSCredential — exist to keep passwords out of plaintext variables, script files, and command history. This page covers the safe patterns; treat anything that puts a real password in plaintext (even briefly) as a bug to fix, not a shortcut to take.

Basic Syntax

$credential = Get-Credential
$securePassword = Read-Host -AsSecureString "Enter password"
$credential | Export-Clixml -Path "C:\Secure\cred.xml"
$credential = Import-Clixml -Path "C:\Secure\cred.xml"

Key Points

  • SecureString encrypts a value in memory; it is not the same as encrypting it on disk
  • Export-Clixml on a credential encrypts it with Windows DPAPI, tied to the current user AND machine
  • A credential exported on one machine cannot be imported and decrypted on another
  • ConvertTo-SecureString -AsPlainText -Force is for building test data, not for storing real secrets

Getting Credentials Interactively

# Prompts with a proper Windows credential dialog / console prompt
$credential = Get-Credential

# Pre-fill the username, only prompt for the password
$credential = Get-Credential -UserName "svc-backup"

# Custom prompt message
$credential = Get-Credential -Message "Enter credentials for the backup service"

# Access the pieces
$credential.UserName
$credential.Password          # SecureString - not readable directly
$credential.GetNetworkCredential().Password   # plaintext, use sparingly and only in-memory

GetNetworkCredential().Password Defeats the Point

# Only do this when a specific API genuinely requires a plaintext string,
# and never log, write, or display the result
$plainPassword = $credential.GetNetworkCredential().Password
Calling .GetNetworkCredential().Password decrypts the password back to a plaintext string in memory. It's sometimes unavoidable (some .NET APIs only accept plaintext), but treat the resulting variable as radioactive — don't Write-Output it, don't log it, and let it go out of scope as soon as possible.

Building SecureStrings

# Prompt the user (safest - password never touches script code or history)
$securePassword = Read-Host -AsSecureString "Enter password"

# Build from a known plaintext value (only for test/bootstrap scenarios)
$securePassword = ConvertTo-SecureString "TempP@ss123!" -AsPlainText -Force

# Combine into a PSCredential object
$username = "svc-backup"
$credential = New-Object System.Management.Automation.PSCredential($username, $securePassword)

# PowerShell 7+ shorthand
$credential = [PSCredential]::new($username, $securePassword)

Storing Credentials Securely

Export/Import with Clixml (DPAPI)

# Save a credential, encrypted to the current user + machine
$credential = Get-Credential
$credential | Export-Clixml -Path "C:\Secure\backup-credential.xml"

# Later, in an unattended script, load it back
$credential = Import-Clixml -Path "C:\Secure\backup-credential.xml"

# Use it
$session = New-PSSession -ComputerName "server01" -Credential $credential

DPAPI Credentials Are User- and Machine-Bound

A credential exported with Export-Clixml can only be decrypted by the same Windows user account on the same machine that exported it. This is a feature, not a limitation — it means the exported .xml file is useless to anyone who copies it elsewhere. It also means you can't just copy a credential file to another server and expect it to work; each machine needs its own export, run under the account that will actually use it.

For Cross-Machine or Team Secrets

# Windows Credential Manager (per-user, cross-script on the same machine)
# Requires the CredentialManager module: Install-Module CredentialManager
New-StoredCredential -Target "MyApp" -UserName "svc-app" -Password "P@ssw0rd" -Persist LocalMachine
$credential = Get-StoredCredential -Target "MyApp"

# For real secret management across machines/teams, use a dedicated vault
# instead of file-based storage - e.g. Azure Key Vault
# Install-Module Az.KeyVault
$secret = Get-AzKeyVaultSecret -VaultName "MyVault" -Name "BackupServicePassword" -AsPlainText

Pick the Right Tool for the Blast Radius

A single script on a single machine, run by a single person: Export-Clixml/Import-Clixml is fine. Multiple scripts on the same machine sharing a secret: Windows Credential Manager. Secrets shared across a team or multiple servers: a real secret manager (Azure Key Vault, HashiCorp Vault, etc.) — not a shared file, however "encrypted."

Using Credentials

# Remote sessions
$session = New-PSSession -ComputerName "server01" -Credential $credential
Invoke-Command -ComputerName "server01" -Credential $credential -ScriptBlock { Get-Service }

# Authenticating a web request
$headers = @{ Authorization = "Basic " + [Convert]::ToBase64String(
    [Text.Encoding]::ASCII.GetBytes("$($credential.UserName):$($credential.GetNetworkCredential().Password)")
)}
Invoke-RestMethod -Uri "https://api.example.com/data" -Headers $headers

# Running a scheduled task as a specific account
Register-ScheduledTask -TaskName "Backup" -Action $action -Trigger $trigger `
    -User $credential.UserName -Password $credential.GetNetworkCredential().Password

Important Parameters

Parameter Type Description Example
-UserName String Get-Credential: pre-fill the username Get-Credential -UserName "svc"
-Message String Get-Credential: custom prompt text -Message "Enter creds"
-AsSecureString Switch Read-Host: mask input and return SecureString Read-Host -AsSecureString
-AsPlainText Switch ConvertTo-SecureString: input is plaintext -AsPlainText -Force
-Path String Export/Import-Clixml: file location Export-Clixml -Path "cred.xml"
-Credential PSCredential Pass to remoting/auth cmdlets Invoke-Command -Credential $cred

Common Patterns

# Pattern 1: Bootstrap a credential file once, reuse it in unattended scripts
$credPath = "C:\Secure\backup-credential.xml"
if (-not (Test-Path $credPath)) {
    (Get-Credential -Message "One-time setup: enter backup account credentials") |
        Export-Clixml -Path $credPath
}
$credential = Import-Clixml -Path $credPath

# Pattern 2: Fall back to prompting if no stored credential exists
$credPath = "C:\Secure\app-credential.xml"
$credential = if (Test-Path $credPath) {
    Import-Clixml -Path $credPath
} else {
    Get-Credential
}

# Pattern 3: Build a credential from separate username/password variables
$username = "svc-app"
$securePassword = ConvertTo-SecureString $env:APP_PASSWORD -AsPlainText -Force
$credential = [PSCredential]::new($username, $securePassword)

Real-World Examples

Example: Unattended Script Using a Pre-Staged Credential

Scenario: A scheduled backup script needs to authenticate to a remote server without any human present to type a password.

$credPath = "C:\Secure\backup-credential.xml"

if (-not (Test-Path $credPath)) {
    throw "No stored credential found at $credPath. Run the one-time setup script first."
}

$credential = Import-Clixml -Path $credPath

try {
    $session = New-PSSession -ComputerName "fileserver01" -Credential $credential
    Invoke-Command -Session $session -ScriptBlock {
        Copy-Item -Path "D:\Backups\*.bak" -Destination "\\archive\backups\" -Force
    }
} finally {
    if ($session) { Remove-PSSession -Session $session }
}

Explanation: The credential is set up once, interactively, by a human (see Pattern 1 above), then loaded silently by the scheduled task. Because Export-Clixml ties the encryption to the specific user account and machine running the scheduled task, the stored file is useless if stolen and copied elsewhere.

Tips & Tricks

Clear Plaintext Variables When You're Done

$plainPassword = $credential.GetNetworkCredential().Password
# ... use $plainPassword for the one API call that needs it ...
$plainPassword = $null
Remove-Variable plainPassword -ErrorAction SilentlyContinue
This doesn't guarantee the memory is scrubbed (garbage collection timing is not deterministic), but it minimizes how long a plaintext password variable stays reachable and reduces the chance of it ending up in a stack trace or accidental Write-Output.

Command History Remembers Everything You Type

# BAD - now sitting in plaintext in (Get-PSReadlineOption).HistorySavePath
$securePassword = ConvertTo-SecureString "RealPassword123" -AsPlainText -Force

# GOOD - never type the real password as literal text
$securePassword = Read-Host -AsSecureString "Enter password"
PSReadLine saves command history to disk by default. Any plaintext password typed directly into a command — even inside ConvertTo-SecureString -AsPlainText— is preserved in that history file. Always get real passwords via prompt, environment variable, or a vault, never as literal text in a command.

Additional Resources