Handling Credentials Safely in PowerShell — Banishing Plaintext Passwords from Your Scripts
· Go Komura · PowerShell, Windows, Security, Credentials, DPAPI, Automation, Operational Improvement, Script
When we look at a customer’s PowerShell scripts during a bug investigation or a script review, there is one thing we run into with remarkable regularity: $password = "P@ssw0rd123". A connection to a file server, the database behind the core business system, sending email, a web API key. Getting it working took priority, and the password ended up embedded in plaintext in the script, living on for years in a shared folder or a Git repository.
The awkward part is that the person who wrote it already knows it isn’t good. But what is the right thing to do instead? SecureString, Export-Clixml, SecretManagement, Credential Manager, Azure Key Vault — the options pile up, and it is hard to tell what each of them actually covers and where each of them stops. On top of that you hear “SecureString isn’t recommended any more”, and it becomes unclear what to believe. There are reasons for this confusion, and once you lay it out the path is clear.
This article is aimed at in-house IT and operations staff who look after internal operations scripts and scheduled batch jobs. It starts from what is actually wrong with plaintext passwords, organises PowerShell’s credential tooling mechanism by mechanism, and pulls the realistic answers for “what do I do for unattended runs?” into a decision table. PowerShell 7.x is the baseline, with notes for sites still on Windows PowerShell 5.1.
1. The Bottom Line First
- The problem with a plaintext password is that the leak is confirmed the moment the file becomes visible. Git history, shared folders, backups, logs — every path along which copies of the script multiply is a leak path. Writing code that converts plaintext into a SecureString (
ConvertTo-SecureString -AsPlainText) solves nothing if the original plaintext still sits in the script or the log.12 - For scripts used interactively, the basic pattern is to take a PSCredential from Get-Credential. The password is never displayed on screen, and it can be passed as an object to each command’s
-Credentialparameter.3 - The official .NET documentation states plainly that SecureString is not recommended for new development. The encryption only happens on Windows; on non-Windows platforms the internals are not encrypted. PowerShell, meanwhile, keeps using SecureString for compatibility, so you will be living with it as the standard hand-off format for the foreseeable future. Don’t over-trust it, and don’t build your own protection mechanism on top of it.45
- For unattended runs, saving credentials to a file with Export-Clixml and DPAPI encryption is the minimal practical answer. It can only be decrypted by the user who saved it, on the machine where it was saved, so the file on its own is useless if it leaks. The flip side is that it has to be saved by the Task Scheduler run-as account itself. On non-Windows platforms nothing is encrypted.6
- Once you are handling multiple secrets, centralise them with SecretManagement plus SecretStore. The unified Set-Secret / Get-Secret interface lets you swap the storage backend from a local SecretStore all the way to Azure Key Vault. For unattended runs, though, handling the vault password remains an open problem.78
- The very first thing to consider is a design that holds no credentials at all. Give the run-as account itself (a domain account or a gMSA) rights on the target and the password disappears from the script. With a gMSA, password management itself is handed over to the OS.910
- Build leakage into logs and transcripts into your design. The official documentation warns that enabling script block logging can write sensitive data, including credentials used in scripts, into the event log. The moment you type a plaintext password on a command line, assume it has been recorded.11
2. What’s Wrong with Plaintext — There Are as Many Leak Paths as There Are Copies
First, let’s look at the typical pattern that needs rewriting.
# Anti-pattern: both lines are equally bad, because the plaintext stays in the script
$password = "P@ssw0rd123"
# Converting to a SecureString doesn't help — the original plaintext is still on line 1.
# PSScriptAnalyzer flags this use of -AsPlainText as an error
$secure = ConvertTo-SecureString $password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('svc-transfer', $secure)
Hard-coding a password isn’t dangerous only because “a malicious intruder might read it”. Because of what a script is as an artefact, copies multiply simply through legitimate day-to-day operations.
| Where copies come from | What happens |
|---|---|
| Git repository | A password committed once is preserved in the history forever. Fixing the file afterwards doesn’t remove it from the history |
| Shared folder | Spreads to everyone with read access, plus backups and generation copies |
| Email and chat | The moment you attach it with “here, use this script”, it is out of your control |
| Event log | With script block logging enabled, the contents of executed code are recorded in the log11 |
| Transcripts | Start-Transcript, or an organisation-wide transcription policy, records whatever went across the screen11 |
What this structure means is that you cannot solve the plaintext password problem by “making sure nobody can see the file”. However tightly you restrict access rights, you can’t close off Git history and backups. The real answer is to get the password out of the script and into encrypted storage. It is the same argument we made about configuration files for .NET applications in “Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI”, and the principle doesn’t change for PowerShell.
Note that converting plaintext to a SecureString on the spot — ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force — is not a solution. PSScriptAnalyzer (the official static analysis tool) flags this style as an error (Severity: Error). It bypasses the encryption and exposes plaintext in memory, and the original plaintext continues to live in the script.1
3. What the Tools Really Are — PSCredential, SecureString, and Their Limits
3.1. Get-Credential and PSCredential
For an interactive script, this is the whole basic pattern.
# Prompt for a username and password, and receive them as a PSCredential object
$cred = Get-Credential -Message 'Enter the account used to connect to the core database'
# It can be passed straight to any command that has a -Credential parameter
Invoke-Command -ComputerName APPSV01 -Credential $cred -ScriptBlock { hostname }
Get-Credential prompts for a username and password and returns a PSCredential object. On Windows PowerShell 5.1 this is a dialog; on PowerShell 6 and later it is console input.3 The password is held inside the PSCredential as a SecureString and is never displayed on screen. Where “a person types it in at the time” is acceptable, there is no need to do anything more complicated than this.
If you are writing your own functions, design them to take a [PSCredential] -Credential parameter rather than a password as a [string]. The official article “Add Credential support to PowerShell functions” covers how to write this.2
3.2. What SecureString Really Is — How to Read the “Not Recommended” Verdict
There are three things about SecureString that you need to know.
- The official .NET position is “don’t use it in new development”. The internal array is only encrypted on Windows; on non-Windows platforms the internal storage is not encrypted. And since it has to be converted to a plaintext representation at the moment of use anyway, all it really achieves is shortening the exposure window. The recommended alternative is “an opaque handle to a credential stored outside of the process” — in other words, an OS credential store or something like Key Vault.4
- PowerShell’s standard tooling nonetheless assumes SecureString. PowerShell continues to support SecureString for compatibility, and it is still used to avoid accidental exposure in the console or in logs. The positioning is “safer than a plaintext string”.5
- It converts back to plaintext trivially. In PowerShell 7,
ConvertFrom-SecureString -AsPlainTextgets you plaintext in one step.12 Treating SecureString as “an envelope so you don’t see it by accident” rather than “an unreadable safe” is much closer to the truth.
The practical conclusion is this. Don’t let SecureString push you into building complicated encryption of your own. Use it as the format PowerShell’s tools (Get-Credential, SecretManagement) require, and put the actual protection into out-of-process mechanisms like DPAPI or a vault.
4. The Standard for Unattended Runs — Export-Clixml, DPAPI, and the “Same User, Same Machine” Wall
An unattended script running under Task Scheduler can’t very well ask a human with Get-Credential. The minimal practical answer is a credential file created with Export-Clixml.
# --- Preparation (once only, run as the task's run-as account) ---
$cred = Get-Credential -Message 'Integration account'
$cred | Export-Clixml -Path 'D:\Jobs\secrets\transfer.credential'
# --- Production script (unattended) ---
$cred = Import-Clixml -Path 'D:\Jobs\secrets\transfer.credential'
# Example: connecting to a share that needs different credentials, or remote execution
Invoke-Command -ComputerName FILESV01 -Credential $cred -ScriptBlock { Get-ChildItem D:\Export }
Export-Clixml saves the credential object encrypted with Windows DPAPI (the Data Protection API). It can only be decrypted when the user account that saved it opens it, on the computer where it was saved. The exported file cannot be used on another machine or by another user.613 The property that a stolen file cannot be opened is exactly what makes it convenient as storage for unattended runs.
That said, “same user, same machine only” is both the protection and an operational trap.
- It has to match the Task Scheduler run-as account. A
.credentialfile you created under your own account stops decrypting the moment the task runs as a service account. Always do the preparation step as the task’s run-as account (start PowerShell as that account and save the file there). Run-as accounts and logon types for tasks are covered in detail in “When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation”. - A server replacement or an account change always means recreating it. This is a classic cause of “it stopped working after the migration”, so script the preparation procedure and keep it in the repository (without the password itself, obviously).
- Any process running as the same account can decrypt it. DPAPI is open to any code running as that user, so the surrounding hygiene still matters: don’t co-locate unnecessary software under the run-as account, and keep the account’s privileges minimal.
- On non-Windows platforms nothing is encrypted. The official documentation states explicitly that on macOS and Linux the output is effectively plaintext (a Unicode character array).6
There is also the approach of passing -Key to ConvertFrom-SecureString for AES encryption, but that tends to just shift the same problem one step sideways: now you have to protect the key file.12 As soon as you need to cross machine boundaries, move on to the vault in the next section, or to the “hold nothing” design in Section 6.
5. When the Secrets Pile Up — SecretManagement and SecretStore
As the number of targets grows and API keys and tokens get mixed in, scattered .credential files hit their management limit. That is where the SecretManagement module comes in. It is a unified interface to secret storage (vaults), with the actual storage handled by extension vaults. Alongside the locally stored SecretStore (from Microsoft), you can drive extension vaults such as Azure Key Vault and KeePass through the same commands.714
# First time only: install the modules and register a vault
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore
Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
# Register a secret (you'll be asked to set the vault password on first access)
Set-Secret -Name TransferJobCred -Secret (Get-Credential)
# In the script: retrieve it by name. This line doesn't change when the backend does
$cred = Get-Secret -Name TransferJobCred
Besides PSCredential and SecureString, secret values can be strings, byte arrays, or hashtables, and on first access you are prompted to set a password protecting the vault itself.15 The benefit is that knowledge of “where it is stored” disappears from the script. Using SecretStore on a development machine and Azure Key Vault in production (the Az.KeyVault module provides the extension vault) becomes a matter of changing the Register-SecretVault configuration and nothing else.167
There are, however, three things to watch when you take this into unattended execution.
- SecretStore asks for the vault password interactively by default. Drop it into Task Scheduler as-is and it will hang waiting at the prompt. For unattended runs the official documentation describes setting Interaction to
Noneand passing the vault password — parked in a DPAPI-protected file created with Export-Clixml — via Unlock-SecretStore.8 In other words, the vault key ends up protected by DPAPI after all, inheriting the same-user, same-machine constraint from the previous section. It is also possible to disable the password requirement entirely (Authentication None), but then the key is protected by file system permissions alone, so this isn’t recommended where strong protection is required.17 - It doesn’t work with managed accounts such as gMSAs. SecretManagement depends on the user profile (
$env:LOCALAPPDATA) and DPAPI, and it is documented as not currently supporting Windows managed accounts, which have no profile.14 If you want a job running under a gMSA to hold secrets, this combination isn’t available to you (and in any case, a gMSA usually lets you shift towards a design that holds no secrets — see the next section). - SecretManagement and SecretStore are considered feature complete, and active development of new features has ended. Security fixes and critical bug fixes continue, so there is no problem using them; but Microsoft itself notes that “the nature of secrets is shifting towards passwordless and federated credentials”, so for long-term design you should also have a look at the authentication method itself (Entra ID managed identities and the like).7
There are also community extensions that use the Windows Credential Manager as a vault.7 As we touched on in “Pitfalls of Network Drives and UNC Paths — Working With File Servers (Shared Folders) From a Business Application” — for example how credentials registered with cmdkey get used automatically — Credential Manager behaves the same way here: it is per user profile.
6. The Highest-Priority Answer — Don’t Hold Credentials at All
We’ve been building up “how to store it safely”, but the cleanest answer for unattended execution isn’t actually a clever storage technique. It is removing the need for the script to hold a credential at all.
Windows tasks and services run in the security context of their run-as account. If the target is protected by Windows Authentication — a shared folder ACL, SQL Server Windows Authentication, Windows Integrated Authentication on an internal API — then granting the run-as account itself rights on the target means neither a password nor a Get-Secret ever appears in the script. Kerberos handles the authentication under the run-as account’s identity for you.
The thing that makes this work is the gMSA (group Managed Service Account). A gMSA is a managed domain account where password management is taken over by the Windows OS.9 The password is a 240-byte random value that the OS rotates automatically every 30 days, so nobody knows it and nothing breaks because of expiry.10 And gMSAs can be used not only for Windows services but for Task Scheduler tasks as well.18
Laid out as an order of preference, it comes to this.
- If the target can use Windows Authentication, remove the credential by granting rights to the run-as account (a domain account or gMSA). For unattended jobs in a domain environment, consider this first.9
- Only store a secret for the targets where that isn’t possible (a SQL-authentication database, an API key, a workgroup NAS, and so on). One or two secrets: Export-Clixml with DPAPI. Several: SecretManagement plus SecretStore.68
- For connections to cloud resources, prefer managed identities or federated credentials over storing keys. If you must store them, put them in a dedicated vault such as Azure Key Vault.167
Where you explicitly pass credentials to a remote server to run work there, PowerShell Remoting’s authentication mechanism also comes into play. See “An Introduction to PowerShell Remoting (WinRM) — Managing Multiple Windows Machines at Once”, published alongside this article.
7. Keeping Secrets Out of Logs and Transcripts
Locking down storage is pointless if the secret leaks through the runtime record instead. There are two points to cover.
First, PowerShell has several features that record what was executed, and they may be enabled by organisational policy. Enabling script block logging records the contents of every script block processed into the event log. The official documentation warns explicitly that “enabling script logging may write sensitive data, including credentials used in scripts, into the event log”, and recommends combining it with Protected Event Logging for anything beyond diagnostics.11 The same applies to transcription: whatever went across the console is left in a file.
Second, and consequently, be rigorous about writing code so that plaintext never passes through a command line or the screen.
- Don’t design scripts to take a password as an argument. The moment you type
.\job.ps1 -Password "P@ssw0rd", assume it is in the logs, the history, and the process list. If you must accept it, accept a PSCredential or a SecureString.2
# House style for your own script's entry point: don't accept the password as a [string]
[CmdletBinding()]
param(
# Accept credentials as a PSCredential. If omitted, obtain them interactively with
# Get-Credential; for unattended runs, pass the result of Import-Clixml or Get-Secret
[Parameter(Mandatory)]
[System.Management.Automation.PSCredential]$Credential
)
# Never emit variables containing secrets to debug output. The username is as far as it goes
Write-Verbose "Connecting as: $($Credential.UserName)"
- Don’t dump variables containing secrets with Write-Host or Write-Verbose. That “just temporary” output you added while troubleshooting gets made permanent in a transcript.
- Watch out for secrets bleeding into exception messages. If you build a connection string and then throw, the caught error tends to be logged with the connection string still in it. For designing what your error handling actually writes to the log, see “PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices” as well.
8. Practical Rules of Thumb (Decision Table)
| Situation | Recommendation | How to judge |
|---|---|---|
| A person runs it there and then | Get-Credential | Not storing it is the safest option. Take a PSCredential and pass it to -Credential3 |
| Unattended job inside a domain (target uses Windows Authentication) | Grant rights to the run-as account / gMSA | A design that holds no credentials comes first. gMSAs work with Task Scheduler too918 |
| Unattended job with one or two secrets | Export-Clixml with DPAPI | Save it as the task’s own run-as account. Recreate on a machine or account change6 |
| Unattended job with many secrets, or a backend you may want to swap later | SecretManagement + SecretStore | Supply the vault password from DPAPI storage via Unlock-SecretStore. Not usable with gMSAs814 |
| Secrets shared across cloud resources or multiple servers | Azure Key Vault (+ SecretManagement) | Machine-local DPAPI can’t be shared. Use it once you need central management and auditing16 |
| Plaintext in the script plus ConvertTo-SecureString | Rewrite it | A bad shape that PSScriptAnalyzer flags as an error. Move to one of the above1 |
When in doubt, work down the order “don’t hold it > hold it pinned to the machine (DPAPI) > hold it in a vault”, starting from the top.
9. Summary
- The fundamental problem with plaintext passwords is that every path along which copies multiply — Git history, shared folders, logs — is a leak path. Access rights management alone can’t protect you.
- Interactive execution is fine with Get-Credential plus PSCredential. Don’t write your own functions that take a password as a
[string]. - SecureString is not recommended for new development according to official .NET guidance, yet it survives as PowerShell’s standard hand-off format. Accept it as “an envelope that prevents accidental exposure” and put the actual protection into external mechanisms.
- Export-Clixml with DPAPI is “same user, same machine only”. The key point is that the saved file must be created by the Task Scheduler run-as account itself, and on non-Windows platforms nothing is encrypted.
- SecretManagement plus SecretStore is effective for centralising multiple secrets, but unattended runs need a design for supplying the vault password, and it can’t be used with gMSAs. Factor in that it is considered feature complete when you adopt it.
- The top priority is a design that holds no credentials. Consider first whether Windows Authentication plus rights granted to the run-as account (a gMSA) can remove passwords from the script entirely.
- Script block logs and transcripts can retain sensitive data. Be rigorous about writing code where plaintext never passes through a command line, the screen, or an exception message.
Related Articles
- Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI
- When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation
- Pitfalls of Network Drives and UNC Paths — Working With File Servers (Shared Folders) From a Business Application
- An Introduction to PowerShell Remoting (WinRM) — Managing Multiple Windows Machines at Once
- PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices
- Parameter Design and Modularization for PowerShell Scripts — From a Script That Works to a Script You Can Hand Over
Related Consulting Areas
KomuraSoft LLC handles auditing the credentials embedded in operations scripts and scheduled batch jobs and migrating them to safe storage, run-as account design including gMSAs, and security reviews of unattended jobs. Sorting out plaintext passwords left alone because “it works, so don’t touch it” is a fine place to start.
- Technical Consulting & Design Review
- Windows Modernization & Maintenance
- Bug Investigation & Root Cause Analysis
- Contact
References
-
Microsoft Learn, AvoidUsingConvertToSecureStringWithPlainText. On how PSScriptAnalyzer flags the use of ConvertTo-SecureString -AsPlainText as an error (Severity: Error), how it bypasses encryption and exposes sensitive information as plaintext in memory, and how Read-Host -AsSecureString and the SecretStore module are given as alternatives. ↩ ↩2 ↩3
-
Microsoft Learn, Add Credential support to PowerShell functions. On how to add a PSCredential parameter to your own functions, and why using ConvertTo-SecureString -AsPlainText draws a warning, because plaintext passwords end up recorded in various logs. ↩ ↩2 ↩3
-
Microsoft Learn, Get-Credential. On how Get-Credential prompts for a username and password and returns a PSCredential object, how Windows PowerShell 5.1 uses a dialog while PowerShell 6 and later prompt in the console, and how the result is passed to commands that have a -Credential parameter. ↩ ↩2 ↩3
-
Microsoft Learn, SecureString Class. On the recommendation not to use SecureString in new .NET (Core) development, how encryption only happens on Windows and internal storage is not encrypted on non-Windows platforms, how a conversion to a plaintext representation is needed at the point of use, and how the recommended alternative is an opaque handle to a credential stored outside the process. ↩ ↩2
-
Microsoft Learn, Advisory Development Guidelines. On how .NET does not recommend new use of SecureString while PowerShell continues to support it for backward compatibility, how it is safer than a plaintext string and is used to avoid accidental exposure in the console or in logs, and how it should be used carefully because it converts back to plaintext easily. ↩ ↩2
-
Microsoft Learn, Export-Clixml. On how Export-Clixml saves credential objects encrypted with Windows DPAPI, how decryption is only possible for the user account that saved it on the computer where it was saved so the exported file cannot be used on another machine or by another user, and how on non-Windows platforms (macOS/Linux) nothing is encrypted and the output is effectively plaintext. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Overview of the SecretManagement and SecretStore modules. On how SecretManagement is a unified interface to extension vaults, how SecretStore is a cross-platform extension vault that stores secrets encrypted in a local file, how extension vaults exist for Azure Key Vault, KeePass, Credential Manager (CredMan) and others, and how the Secret modules are considered feature complete with active development ended and only security fixes continuing. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Use the SecretStore in automation. On configuring SecretStore’s Interaction to None for unattended runs, the pattern of saving the vault password in a DPAPI-encrypted file created with Export-Clixml and unlocking with Unlock-SecretStore, and how this is a Windows-only solution. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Group Managed Service Accounts overview. On how a gMSA is a managed domain account that provides automatic password management and simplified SPN management, with password management handled by the Windows OS rather than by an administrator. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Secure group managed service accounts. On how a gMSA password is a 240-byte random value, how the OS changes it automatically every 30 days so no password-change planning or service outage is needed, and how gMSAs are recommended as the account type for on-premises services. ↩ ↩2
-
Microsoft Learn, about_Logging_Windows. On how enabling script block logging records the contents of every script block PowerShell processes into the event log, how raising the logging level can include sensitive data such as credentials used in scripts, and on Protected Event Logging as a protection for that. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, ConvertFrom-SecureString. On converting a SecureString into an encrypted standard string, how Windows DPAPI is used when no key is specified and AES when Key or SecureKey is given, and how -AsPlainText converts directly to a plaintext string. ↩ ↩2
-
Microsoft Learn, Import-Clixml. On how Import-Clixml restores credentials and secure strings saved by Export-Clixml, and how this avoids the risk of writing plaintext passwords into a script. ↩
-
Microsoft Learn, Understanding the SecretManagement module. On how SecretManagement stores and retrieves secrets through registered extension vaults, how vault registration is per user context, and how its dependency on $env:LOCALAPPDATA and DPAPI means it does not currently work with Windows managed accounts. ↩ ↩2 ↩3
-
Microsoft Learn, Get started with the SecretStore module. On registering SecretStore with Register-SecretVault, the basic Set-Secret / Get-Secret / Get-SecretInfo operations, and being prompted to set the vault password on first access. ↩
-
Microsoft Learn, Use Azure Key Vault in automation. On how Az.KeyVault 3.3.0 and later includes a SecretManagement extension, letting you register Azure Key Vault as a SecretManagement vault with Register-SecretVault and drive it with Get-Secret and friends. ↩ ↩2 ↩3
-
Microsoft Learn, Understanding the security features of SecretManagement and SecretStore. On how, when password authentication is disabled entirely, the decryption key is protected by file system permissions alone, which is not recommended for systems requiring strong security protection. ↩
-
Microsoft Learn, Manage group Managed Service Accounts. On how gMSAs can be used for services configured in Service Control Manager, IIS application pools, and Task Scheduler tasks. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
An Introduction to PowerShell Remoting (WinRM) — Managing Multiple Windows Machines at Once
An introduction to managing multiple Windows machines at once with PowerShell Remoting (WinRM). Covers how it works and ports 5985/5986, ...
PowerShell Execution Policy and Script Signing — A Practical Guide to Graduating From "Papering Over It With Bypass"
PowerShell's execution policy is "a safety feature, not a security boundary." This article covers the differences between policies like R...
Where to Look When a PowerShell Script Is Slow — Arrays, Pipelines and Matching
The classic causes of slow PowerShell scripts, laid out. Why += on an array is O(n^2), the difference between the pipeline and foreach, t...
Stop Using Write-Host — PowerShell Output Streams and Log Design
How to choose between PowerShell's six output streams, the problems with Write-Host and where it genuinely belongs, why function return v...
Parallel Processing in PowerShell — Choosing Between ForEach-Object -Parallel and Jobs
A practical rundown of the differences between ForEach-Object -Parallel, Start-ThreadJob and Start-Job and when to use each, $using: and ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Why shouldn't I write a password in plaintext in a PowerShell script?
- Because a script is an artefact that is, by its nature, copied, shared, and recorded in history. Commit a password to Git once and removing it from the history is difficult; put the script in a shared folder and everyone with read access can see it; and script block logging and transcripts record the contents of commands verbatim. In other words, a plaintext password creates a structure where the leak is confirmed the moment the file becomes visible. The real fix isn't to close off the leak paths one by one — it's to move the password out of the script and into protected storage.
- How safe is a credential saved with Export-Clixml?
- On Windows it is encrypted with DPAPI (the Data Protection API), and it can only be decrypted by the user account that saved it, on the same computer. Even if the file is stolen, it cannot be opened on another machine or by another user, which makes it a practical place to store credentials for unattended runs. It isn't a cure-all, though: any process running as the same account can decrypt it, and on macOS and Linux nothing is encrypted, so the output is effectively plaintext. If you are going to use it from Task Scheduler, the saved file has to be created by the task's own run-as account.
- Should I still be using SecureString?
- The official .NET documentation states plainly that SecureString is not recommended for new development. The encryption only happens on Windows; on non-Windows platforms the internal storage is not encrypted. And at the moment you actually use it, you have to convert it back to plaintext anyway. On the other hand, the standard tooling in the PowerShell world — Get-Credential, SecretManagement and so on — is built around SecureString, and it does reduce exposure compared with carrying a plaintext string around, so for the time being the realistic stance is to treat it as "PowerShell's standard hand-off format". Just don't use it as the raw material for a protection scheme of your own.
- Won't SecretStore stop and ask for a password during an unattended Task Scheduler run?
- With the default configuration, yes. SecretStore requires a vault password by default and raises an interactive prompt. For unattended runs, the official documentation describes setting Interaction to None and unlocking with Unlock-SecretStore, reading the vault password from a DPAPI-protected file (created with Export-Clixml). Note that this is a configuration that protects the vault key with DPAPI, so it inherits DPAPI's constraints (same user, same machine). Before you go there, consider whether a gMSA or granting rights to the run-as account could remove the need for a credential in the first place.
- Is there a way to avoid storing credentials at all?
- There is — and it should be your first choice. Windows tasks and services run under the security context of their run-as account, so if you grant that account the rights it needs on the target (a shared folder, SQL Server Windows Authentication, and so on), the script never has to hold a password. Make the run-as account a gMSA (group Managed Service Account) and the password becomes a 240-byte random value that the OS rotates automatically every 30 days, so nobody knows it at all. gMSAs can also be used for Task Scheduler tasks.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links