“Just for now.”
Those three words got us into this mess.
In our last post, we walked through all the places secrets go to die: registry keys, scheduled tasks, environment variables, PowerShell history files. If it was convenient, we put a password there because the script needed to run, the backup had to fire, or the token just needed to work one more time.
Now it’s time to do better.
This post isn’t about shame. It’s about options. Secure, repeatable, automation-friendly options for storing and retrieving secrets without leaving them lying around for anyone (or any malware) to find. Whether you’re managing one system or automating at scale, here are the tools, patterns, and tradeoffs to know.
What Secure Enough Looks Like
Let’s get real: no solution is perfect. But good secret hygiene comes down to three things:
- Keep secrets off disk in plaintext
- Restrict access to only what’s needed, when it’s needed
- Store in a system that supports automated retrieval with auditing or encryption
We’re defending against local snooping, accidental Git commits, over-permissive access, and plain old human forgetfulness. Our goal is to reduce the blast radius when (not if) something slips.
Windows Credential Manager: Built-In and Basic
For local, interactive scripts, Credential Manager is a decent first step. It’s easy to use, encrypted with DPAPI, and built into Windows.
✅ When to Use:
- Scripts run by the logged-in user
- Storing credentials for internal apps, API calls, or service accounts
💻 Example:
# Save a credential
$cred = Get-Credential
New-StoredCredential -Target "MyAppCreds" -UserName $cred.UserName -Password $cred.GetNetworkCredential().Password -Persist LocalMachine
# Retrieve it later
$stored = Get-StoredCredential -Target "MyAppCreds"
$securePassword = ($stored | ConvertTo-SecureString -AsPlainText -Force)
$psCred = New-Object PSCredential($stored.UserName, $securePassword)It’s not portable across machines or users, but it beats creds.txt.
Secure Strings + Certificates: OS-Level Protections
Secure Strings alone aren’t secure. But combine them with certificates or DPAPI, and you get a local encryption option that can work without vault software.
🔐 Option 1: DPAPI (user-scoped)
$secure = ConvertTo-SecureString "MySecret123!" -AsPlainText -Force
$secure | ConvertFrom-SecureString | Set-Content secret.txtThis can only be decrypted by the same user on the same machine:
$secure = Get-Content secret.txt | ConvertTo-SecureString🛡️ Option 2: Encrypt with a certificate
For broader use or CI pipelines, encrypting with a certificate gives you more control:
Protect-CmsMessage -To CN="My Automation Cert" -Content "SuperSecret123!" > secret.cms
Unprotect-CmsMessage -Path secret.cms📌 Tip: Use certs with non-exportable private keys to tighten control.
PowerShell SecretManagement: One Interface, Many Backends
Microsoft’s SecretManagement module lets you securely fetch secrets from any supported vault using a common command set:
# Install the SecretManagement module
Install-Module -Name Microsoft.PowerShell.SecretManagement -Scope CurrentUser -Force
# Install a vault extension (e.g., built-in encrypted store)
Install-Module -Name Microsoft.PowerShell.SecretStore -Scope CurrentUser -Force
# One-time setup
Register-SecretVault -Name MyVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
# Store a secret
Set-Secret -Name "MySQL-Admin" -Secret "Summer2025!"
# Retrieve later
$secret = Get-Secret -Name "MySQL-Admin"This backend can be:
- The built-in SecretStore (encrypted with DPAPI)
- Azure Key Vault
- KeePass, Bitwarden, LastPass, HashiCorp Vault (with extensions)
Azure Key Vault: Cloud-Scale Secret Storage
When you’re scripting for Azure workloads or cloud-first automation, Azure Key Vault is the gold standard.
With proper role assignments and Managed Identity, your scripts don’t need to store anything. They fetch secrets dynamically from the vault:
Connect-AzAccount -Identity # Assumes a managed identity
Get-AzKeyVaultSecret -VaultName 'MyVault' -Name 'Prod-API-Key'✔️ Secrets are audited
✔️ Access is role-controlled
✔️ No credentials in sight
You can even integrate this with Azure Automation or GitHub Actions for full cloud-native pipelines.
CI/CD Pipelines: Secrets Done Right in DevOps
Hardcoding secrets into GitHub Actions, Azure DevOps, or any pipeline config is asking for trouble. Fortunately, all major platforms support encrypted secrets:
📦 GitHub Actions
env:
API_KEY: ${{ secrets.MY_API_KEY }}⚙️ Azure DevOps
Use Library-secured variables or Key Vault-linked variable groups.
No more copy/pasting secrets into YAML. Store them centrally and reference by name.
Refactoring: The Antidote to Regret
Found a script that still says $password = "Admin2024!"?
Here’s the safer pattern:
Before:
Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer hardcoded-token" }After:
# Import the SecretManagement module
Import-Module -Name Microsoft.PowerShell.SecretManagement
# Store a token securely
Set-Secret -Name "API-Token" -Secret "abc123supersecrettoken"
# Retrieve it later
$token = Get-Secret -Name "API-Token"
Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $token" }Or better yet, pull a fresh token via OAuth each time, using client credentials and not hardcoded secrets at all.
Final Thoughts: No More Excuses
There’s no shortage of ways to store secrets safely. If you’ve ever said, “I’ll fix this later,” this is later.
🔑 Use Credential Manager for local scripts.
🔐 Encrypt with certs or DPAPI when vaults aren’t an option.
🗝️ Use SecretManagement when you want flexibility.
☁️ Use Key Vault when working in the cloud.
⚙️ Keep secrets out of repos and pipelines with proper CI practices.
“Just for now” is how secrets leak. But with these tools, your code can stop spilling and start respecting the sensitive info it handles.
🔧 Tools Mentioned
👣 Next Steps
If your environment is already littered with legacy secrets, circle back to our previous post and use the Find-PlainTextSecrets function to clean up.
Then come back here to future-proof your automation.
🧽 You can’t mop up a leaked secret, but you can write code that never spills in the first place.


