Build Constrained PowerShell Endpoint Using Startup Script

Doctor Scripto

Summary: Learn how to build a constrained endpoint by using a startup script.

Hey, Scripting Guy!, how do I lock-down my remote endpoint to only certain commands?


Honorary Scripting Guy, Boe Prox, here today filling in for my good friend, The Scripting Guy. This is the second part in a series of five posts about Remoting Endpoints. The series includes:

  1. Introduction to PowerShell Endpoints
  2. Build Constrained PowerShell Endpoint Using Startup Script (today’s post)
  3. Build Constrained PowerShell Endpoint Using Configuration File
  4. Use Delegated Administration and Proxy Functions
  5. Build a Tool that Uses Constrained PowerShell Endpoint

As seen at the end of yesterday’s post, I was able to restrict the commands that are available from a remote Windows PowerShell session by using a constrained endpoint. Today I am going to show you how to create a constrained endpoint by using a startup script.

In Windows PowerShell 2.0, this was the only way to configure constrained endpoints, but in Windows PowerShell 4.0 and Windows PowerShell 3.0, this is a more optional approach. By using Register-PSSession with the –StartupScript parameter, you can specify the startup script that will be used when a user makes a connection to the system via the constrained endpoint configuration.

Configuring a startup script may seem tricky at first, and depending on your requirements, it may take a little bit of time to get everything locked down the way you want. But fear not! I will take you through the steps required to make all of this come together.

Let’s start by opening the ISE and giving the startup script a name similar to what we expect to name the session configuration. In this case, I am going to call it ConstrainedSession.ps1.

Image of tab

Now comes the fun part: writing the necessary code to lock down the session to only a few commands and perhaps a couple of other things. Be mindful to the order of things here because the startup script could potentially fail if you call one thing prior to another.

I want to define the cmdlets that I want to have available for anyone who uses this constrained endpoint.

#Define any visible cmdlets that we want visible to the user

[string[]]$PublicCmdlets = 'Get-Process','Get-Service','Clear-Host','Get-Alias'

Nothing special here—I’m simply setting up the white list so I know to keep these available for the users.

Next up is a list of applications that I would also like to have available. These can include anything—such as ping or ipconfig.

#Define applications that I want visible to the user; ie: ping, netstat, etc…

[string[]]$Apps = 'ping','netstat','ipconfig'

[string[]]$PublicApps = ForEach ($app in $apps) {

    Get-Command $app | Select -ExpandProperty Definition


In this case, I am allowing the use of ping, netstat, and ipconfig in the endpoint. The key thing here is to make sure that you have the full path to the executable so it can be made available later in the script. If you do not use a full path, it will not work properly. Also, these are currently being saved as a string[] object, but later I will need to make an adjustment to properly load these into the session.


Image of command output

Like the previous two items, I want to keep a few aliases available. If I had other scripts that I would like to have had available, I would also add them.

#Define any Public Aliases

[string[]]$PublicAliases = 'gsv','gps','ps','exsn','cls','gal'

#Define Public Scripts

[string[]]$PublicScripts = $Null

Now we can start hiding the cmdlets, aliases, and variables.  Only those cmdlets and aliases that we defined earlier as being public will still be visible during the constrained session.

You might be asking why I am not keeping any variables publicly available. The answer to that is that it will not matter whether they are visible because we are running this session in a NoLanguage mode. This means that we cannot access any of the language features of Windows PowerShell to include variables.


Get-Command | ForEach {

    If ($PublicCmdlets -notcontains $_.Name) {

        $_.Visibility = 'Private'




Get-Alias | ForEach {

    If ($PublicAliases -notcontains $_.Name) {

        $_.Visibility = 'Private'




Get-Variable | ForEach {

    $_.Visibility = 'Private'


When the script starts, we need to make sure that the applications (such as ping.exe) and any scripts are wiped from the session state.



Now any defined applications and scripts can be added back into the session.  I mentioned earlier that a string[] object would not be accepted at this point, so I have to cast the object as System.Collections.Generic.IEnumerable[string] so it works properly with the AddRange() method.

If ($PublicApps) {


    ($PublicApps -as [System.Collections.Generic.IEnumerable[string]]))


If ($PublicScripts) {


    ($PublicScripts -as [System.Collections.Generic.IEnumerable[string]]))


Now we can set the language to NoLanguage to lock things down for the session.

$ExecutionContext.SessionState.LanguageMode = "NoLanguage"

I am going to create a default restricted remote server SessionState that I can use to determine what commands I need to keep to ensure that the constrained remote endpoint works properly.

$SessionStateType = [System.Management.Automation.Runspaces.InitialSessionState]

$SessionState = $SessionStateType::CreateRestricted("RemoteServer")

Looking at the $SessionState object, we can see a number of things such as the Language of the restricted session (NoLanguage), and we can take a look at the commands available in this session. Speaking of commands, let’s take a look at those commands and see if anything jumps out.

$SessionState.Commands | Select Name, Visibility, CommandType

Image of command output

Of course, these aren’t all of the commands, but you will notice that all of the cmdlets have their visibility set to Private, which prevents anyone under this restricted session from seeing or using the cmdlets. Now take a look at the last seven commands. They are the only public commands here, and they are labeled as functions. If their names seem familiar, it is because these are proxy functions of the actual cmdlets that were set to Private.

($SessionState.Commands | Group Name | Where {

    $_.count -eq 2

}).Group | Select Name, Visibility,CommandType

Image of command output

These are also the required commands for a working constrained endpoint. I need to hold on to these seven proxy functions for later use.

$proxyfunctions = $SessionState.Commands | Where {

    $_.CommandType -eq 'Function'


The last thing to do in this startup script is to add those proxy functions into the session;

$proxyfunctions | ForEach {

    Set-Item "Function:\$($_.Name)" $_.Definition


Great! Now we will save this script to a location that we know won’t be modified or removed (saving to your profile is probably not a good idea). This way you can edit the script as necessary and not worry about re-registering the session configuration each time. It will automatically load the updated script each time someone connects to the endpoint!

Now let’s register the new session and attempt to connect to it to check things out. You will need the full path to the script for this to be successful.

Register-PSSessionConfiguration -Name ConstrainedSession -StartupScript "C:\PSSessions\ConstainedSession.ps1"

Image of command output

I decided to allow the default groups (Administrators and Remote Management Users) access to this endpoint. If I wanted to allow more access, I can specify the ShowSecurityDescriptorUI parameter. This brings up the familiar Permissions window that we all know and love, and I can make adjustments as needed.

Set-PSSessionConfiguration -Name ConstrainedSession -ShowSecurityDescriptorUI

Image of command output

I’m not really interested in making changes at this point in time, so I will cancel this and continue forward.

Now let’s enter the session and see what we can and cannot do:

Enter-PSSession -ComputerName $env:Computername -ConfigurationName ConstrainedSession

Running various commands yields different results.

Image of command output

You will notice that only a handful of cmdlets are available and the proxy functions that I defined are available to use. Ping is available to use, but nslookup (which was not specified to be public) is not available. The usually available $Pwd variable has now been locked away from me to use. Even the aliases are locked down to only a few publicly available ones.

Image of command output

As you can see, using a startup script to help constrain an endpoint can be very useful (especially if your systems are still running Windows PowerShell 2.0). Now wouldn’t be great if there is a session configuration file that has all of this available to us and would make things much simpler to create and modify? Well, tomorrow’s blog post will cover just that!

GH, that is all there is to using a startup script to enable a constrained endpoint. Remoting Endpoint Week will continue tomorrow when I will talk about building a constrained endpoint by using a configuration file.

I invite you to follow the Scripting Guys on Twitter and Facebook. If you have any questions, send email to me at, or post your questions on the Official Scripting Guys Forum. See you tomorrow.

Boe Prox, Honorary Scripting Guy 


Discussion is closed.

Feedback usabilla icon