This documentation is archived and is not being maintained.
Windows PowerShell Securing the Shell
Don Jones is the Lead Scripting Guru for SAPIEN Technologies and an instructor for ScriptingTraining.com. You can reach Don through his Web site, scriptingAnswers.com.
© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.
When the Windows PowerShell team sat down to create a new shell and the word "scripting" floated across the table, the groans of dismay were probably audible throughout Redmond. After all, previous Microsoft efforts with administrative scripting—I'm speaking of VBScript here—were not exactly problem-free. An
overly permissive execution model, combined with the penchant of most users to log on as full administrators, opened the door for disaster.
"Surely," folks in that first Windows PowerShell™ meeting must have prayed, "we're not going to go through that again." And they haven't. Microsoft has vastly improved its security reputation, in large part because it began thinking about security first rather than later in a product's development cycle. This philosophy is very much evident in Windows PowerShell.
Why Won't My Scripts Run?
Install Windows PowerShell on a fresh computer and double-click a .ps1 file: up pops Notepad, not Windows PowerShell. This is because the .ps1 file name extension—the extension used for Windows PowerShell scripts—has no association with the shell itself. In other words, you can't run a script by simply double-clicking it. The only way to run a script is to open Windows PowerShell, type the script name, and then press Enter.
Actually, just typing the script name isn't sufficient, either. You can see in Figure 1 that the file Demo1.ps1 exists in the current folder, yet typing demo1 and pressing Enter results in an error: "The term 'demo1' is not recognized as a cmdlet, function, operable program, or script file." Why is this? After all, the file is sitting right there.
Figure 1 To avoid command hijacking, Windows PowerShell requires a path to your script (Click the image for a larger view)
This is another aspect of the Windows PowerShell security model. One trick malicious users commonly attempt in other shells is creating a script with the same file name as a built-in command. So, for example, if you wanted a user to run your script, you might name it Dir.ps1 and drop it into a folder. If you convinced the user to type Dir and press Enter, your script could run, not the Dir command the user was expecting. This technique is called command hijacking. In Windows PowerShell, you must always provide a path to such a script, making Windows PowerShell pretty well protected against command hijacking.
Running demo1 doesn't work, since there's no path, but running ./demo1 does work. This is because I've now specified a path—the current folder. That command line is less likely to be confused with a built-in command since you'd never type any kind of path if you were referring to a built-in command. Thus, requiring a path helps Windows PowerShell avoid command hijacking and confusion over what might happen when you press Enter.
A Policy for Script Execution
So you've got a freshly installed copy of Windows PowerShell, you're using the proper syntax, and you attempt to run a script. To your surprise, you're greeted with yet another error message, informing you that Windows PowerShell isn't allowed to run scripts. What??? Welcome to the shell's Execution Policy.
You can see what the current Execution Policy is by running Get-ExecutionPolicy in the shell. By default, it's set to Restricted, which quite simply means that scripts won't run. Ever. For anybody. By default, Windows PowerShell can only be used interactively, rather than to run scripts. You can use the Set-ExecutionPolicy cmdlet to choose one of four possible Execution Policy settings:
- Restricted, the default setting, doesn't allow any scripts to run.
- AllSigned only runs trusted scripts (more on this in a moment).
- RemoteSigned runs local scripts without requiring them to be trusted; scripts downloaded from the Internet, however, must be trusted before they can run.
- Unrestricted allows all scripts to run, even untrusted ones.
Frankly, AllSigned is the lowest setting any production computer should be set to. I find RemoteSigned helpful in development and test environments, but it isn't necessary for the typical user. And I see no need for Unrestricted whatsoever, and wouldn't mind if some future version of Windows PowerShell omitted this overly permissive setting.
It's a Question of Trust
Your computer is preconfigured to trust certain root Certification Authorities (CAs)—that is, servers that issue digital certificates. Figure 2 shows the Internet Options control panel application, which lists trusted root CAs. In Windows Vista®, this list is pretty short and includes only a few major commercial root CAs. By contrast, the default list in Windows® XP is large and includes many root CAs I've never heard of. When a root CA is on this list, you're essentially saying that you trust the company that operates the root CA to do a good job of verifying someone's identity before issuing a digital certificate to them.
When you obtain a Class III digital certificate—commonly called a code-signing certificate—the CA (which may be a commercial CA or a private CA that exists within your organization) must verify your identity. That digital certificate is like an electronic passport or identification card. Before giving me an ID that says I'm Don Jones, for example, the CA needs to take some steps to ensure I really am Don Jones. When you use your certificate to digitally sign a Windows PowerShell script, which you can do using the Set-AuthenticodeSignature cmdlet, you're signing your name to the script. Of course, if you're able to obtain a false certificate containing someone else's name, you can sign his name to the script, which is why it's so important that only trustworthy CAs show up on the list in Figure 2.
Figure 2 Default trusted root CAs in Windows Vista (Click the image for a larger view)
When Windows PowerShell checks to see if a script is trusted—which it does under the AllSigned and RemoteSigned Execution Policy settings—it asks three questions:
- Is this script signed? If not, the script is untrusted.
- Is the signature intact? In other words, has the script changed since it was signed? If the signature is not intact, the script is untrusted.
- Was the signature created using a digital certificate that was issued by a trusted root CA? If not, the script is untrusted.
CMDLET of the Month: Set-AuthenticodeSignature
Set-AuthenticodeSignature is the key to digitally signing your Windows PowerShell scripts, allowing you to use the shell's safest execution policy, AllSigned. To use this cmdlet, you start with another—Get-ChildItem—that fetches an installed code-signing certificate:
$cert = Get-ChildItem –Path cert:\CurrentUser\my –codeSigningCert
You need to replace that file path with one that points to an installed certificate—any installed certificate will be accessible via the cert: drive. Once the certificate is loaded, run this to sign a script:Set-AuthenticodeSignature MyScript.ps1
Of course, provide the complete path to your .ps1 script file. The shell will add a signature block to the file.
How does trust improve security? Sure, a hacker could potentially write a malicious script, sign it, and convince someone in your environment to execute it. Since the script was signed, it will run. But because it was signed, you can use the signature to find the identity of the author and take appropriate action (such as calling law enforcement authorities). A person would have to be extremely stupid, though, to create a malicious script and then put his real name on it!
Of course, if a malicious user were able to get a certificate for someone else's identity—say, from a CA that didn't have a good identity-checking process—you wouldn't be able to use the signature to identify the actual culprit. This is why the root CAs you trust should have an identity-verification process you've found satisfactory.
If you use the AllSigned Execution Policy setting, you even have to digitally sign every script you produce yourself before it will run. The Set-AuthenticodeSignature cmdlet is pretty easy to use, but this can still be quite a hassle. That's where third-party software may come in handy. I recommend you select a script editor or visual development environment that will automatically sign your scripts using the certificate you specify. That way, using signatures is transparent and requires no extra effort. If you'd like suggestions for editors and development environments that support this, visit some of the online scripting forums (such as my site at ScriptingAnswers.com) and post a message asking what other Windows PowerShell fans are using.
Once you have a certificate, you can also run the following one-liner to sign all the scripts in a particular location:
The Windows PowerShell Execution Policy can, obviously, be configured on a computer-by-computer basis. But that's not a good solution for enterprise environments. Instead, you can use Group Policy. (You can download the Windows PowerShell Administrative Templates from go.microsoft.com/fwlink/?LinkId=93675.) Simply drop the file into a Group Policy Object (GPO) that affects your entire domain and set it to Restricted. That way, no matter where Windows PowerShell is installed, you can be sure that scripts won't run. Then you can apply a more permissive setting (say, AllSigned) on just those computers that should be running scripts (such as your own workstation).
Unlike any previous administrative scripting language for Windows, Windows PowerShell even comes prepared to deal with alternate credentials in a secure fashion. For example, the Get-WMIObject cmdlet has a –credential parameter, which allows you to specify an alternate credential for remote WMI connections. The parameter will accept a user name but not a password, thus preventing you from hard-coding sensitive passwords into a clear text script. Instead, when you provide the user name, Windows PowerShell prompts you for the password using a dialog box. If you plan to use an alternate credential again, you can store the authentication information by using the Get-Credential cmdlet:
You'll be prompted immediately for the password and the final credential will be stored in the variable $cred. The $cred variable can then be passed to the –credential parameter as often as needed. You can even use the ConvertTo-SecureString and ConvertFrom-SecureString cmdlets to convert the credential into an encrypted string or to convert a previously encrypted string back into a credential.
The problem with this is that your encryption key winds up being stored in a plain text script, which completely defeats the security. So instead of doing this, you can add a call to Get-Credential to your Windows PowerShell profile. Then, when Windows PowerShell runs, you're immediately prompted for a user name and password, which are then stored in a variable named $cred. This way, you always have a $cred variable available in the shell representing a domain administrator account. And you never have to bother storing that credential anywhere—since the credential is recreated each time you open the shell, there's no need to store it.
Secure by Default
By default, Windows PowerShell is installed and configured with the most secure settings you could probably come up with for an administrative shell that supports scripting. Unless you change any of those settings, Windows PowerShell will stay as secure as it can be. In other words, anything that reduces security in Windows PowerShell is a result of your decision and your action. So before you change any of the defaults—reconfiguring file extension associations, changing the Execution Policy, and so forth—make sure you fully understand the consequences of your actions and be prepared to take responsibility for your changes.