PowerShell 7 offers a number of compelling reasons to adopt it. One of the best is support for SSH as a PowerShell remoting protocol. Unfortunately, this is not a topic that typically comes up for Windows-centric IT Pros. I know this is definitely true in my case, and a deficiency I have been working on. On the plus side, Windows 10 ships with the OpenSSH client installed and ready to go. But what about the OpenSSH Server? On Windows platforms, you need to enable and configure this element if you want to use SSH remoting in PowerShell. Naturally, I'd like to do this with PowerShell and also remotely. First up, I want to show you how to setup the OpenSSH Server component on Windows 10.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Manual Setup
The PowerShell commands for installing the OpenSSH Server component in Windows 10 are pretty simple. First, I need the capability. I'm running these commands interactively.
Get-WindowsCapability -online -name openssh.server* | Add-WindowsCapability -online
I'm using Get-WindowsCapability with a wildcard just in case the version number changes at some point. I can see this worked.
This will setup the sshd service which is not running and configured to start manually. I want to configure the service to start and start automatically.
Set-Service -Name sshd -StartupType Automatic
Start-Service -Name sshd
One thing I have learned about ssh, especially if you want to use it with PowerShell remoting, and that is to verify ssh works outside of PowerShell.
ssh localhost
You might get prompted about accepting keys and for a password. If you're setup is like mine, you'll end up in a CMD prompt. But maybe you'd like a different shell when connecting over ssh. Note that I am not talking about PowerShell remoting over ssh yet. You can modify the registry on your computer.
$regPath = "HKLM:\SOFTWARE\OpenSSH\"
$regProperty = "DefaultShell"
$regValue = (Get-Command powershell.exe).source
Set-ItemProperty -Path $regPath -Name $regProperty -Value $regValue
The change should be immediate and not require restarting the sshd service. Using ssh to connect to localhost now gives me a Windows PowerShell session. It wouldn't take much to assemble a simple PowerShell script to run these commands interactively.
Remote Installation
But I want to do this remotely. I have some Windows 10 VMs that I'd like to connect to with ssh. I will need to use traditional PowerShell remoting. Yes, you could use a third-party tool like psexec.exe or something like Desired State Configuration, but I want to stick with tools and skills you likely already have.
As I was working on this I had a simple PowerShell script that ran the steps I just showed you. I figured all I needed to do was run the script using Invoke-Command. Nope. Turns out the Add-WindowsCapability command will not work in a remoting session. This is apparently a known limitation with feature-on-demand installation. But I wasn't going to let that stop me. My eventual workaround was to create a temporary scheduled task that would add OpenSSH.Server as SYSTEM.
$Time = New-ScheduledTaskTrigger -At 23:59 -Once
$User = New-ScheduledTaskPrincipal System
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument '-noprofile -command "& { Get-WindowsCapability -online -name openssh.server* | Add-WindowsCapability -online}"'
$tmpTask = Register-ScheduledTask -taskpath \ -TaskName "AddTask" -Trigger $Time -Principal $user -Action $Action
Start-ScheduledTask -TaskName AddTask
#give the task a chance to finish
Start-Sleep -Seconds 20
$tmpTask | Unregister-ScheduledTask -confirm:$false
The trigger time is irrelevant because I'm going to manually start the task and then remove it As long as I'm not running this close to midnight, I should be fine.
I also wanted my script to support -WhatIf and offer Verbose output, even though the code was running remotely. Here's the script I came up with.
#requires -version 5.1
<#
This is used to setup SSHD on a Windows 10 Client through PowerShell remoting
Sample usage
Verbose and Whatif
invoke-command -FilePath .\Setup-SSHServer.ps1 -ComputerName win10 -cred $artd -ArgumentList @("Continue",$True)
Verbose
invoke-command -FilePath .\Setup-SSHServer.ps1 -ComputerName win10 -cred $artd -ArgumentList @("Continue")
#>
[cmdletbinding(SupportsShouldProcess)]
param ([string]$VerboseOption = "SilentlyContinue", [bool]$WhatIfOption = $False)
$VerbosePreference = $VerboseOption
$WhatIfPreference = $WhatIfOption
Write-Verbose "Starting SSHD setup process"
Try {
Write-Verbose "Testing for sshd service"
$svc = Get-Service -Name sshd -ErrorAction Stop
if ($svc.status -eq 'Running') {
Write-Verbose "sshd service appears to be installed and running"
}
else {
Write-Warning "sshd service found but it isn't running"
}
#take no further action
Return "OpenSSH Server installation appears to be complete."
}
Catch {
Write-Verbose "sshd service not found. Continuing with installation"
}
Write-Verbose "Testing OpenSSH.Server Windows capability"
if (-Not $svc) {
Try {
$cap = Get-WindowsCapability -Name OpenSSH.Server* -Online -ErrorAction Stop
$cap | Out-String | Write-Verbose
}
Catch {
Throw $_
}
}
if ($cap.State -eq 'NotPresent' -And $cap.name -match "OpenSSH") {
#install capability
Try {
Write-Verbose "Adding $($cap.name)"
if ($pscmdlet.ShouldProcess($cap.Name)) {
#installing with a Scheduledtask as System to get around the limitation
#with Add-WindowsCapability in a PowerShell remoting session
$Time = New-ScheduledTaskTrigger -At 23:59 -Once
$User = New-ScheduledTaskPrincipal System
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument '-noprofile -command "& { Get-WindowsCapability -online -name openssh.server* | Add-WindowsCapability -online}"'
$tmpTask = Register-ScheduledTask -taskpath \ -TaskName "AddTask" -Trigger $Time -Principal $user -Action $Action
Start-ScheduledTask -TaskName AddTask
#give the task a chance to finish
Start-Sleep -Seconds 20
$tmpTask | Unregister-ScheduledTask -confirm:$false
} #Whatif
}
Catch {
Throw $_
}
}
Write-Verbose "Set sshd to auto start"
Try {
if ($pscmdlet.ShouldProcess("sshd", "Configure service")) {
Set-Service -Name sshd -StartupType Automatic -ErrorAction stop
Write-Verbose "Start the sshd service"
Start-Service -Name sshd -ErrorAction Stop
Get-Service sshd | Select-Object -Property * | Out-String | Write-Verbose
}
}
Catch {
Write-Warning "There was a problem configuring the sshd service."
}
if (-Not $WhatIfOption) {
#only display the summary if not using -WhatIf
$msg = @"
SSH setup complete. Edit $ENV:ProgramData\ssh\sshd_config for additional configurations options
or to enable remoting under PowerShell 7.
You will need to restart the sshd service for changes to take effect.
"@
Write-Host $msg -ForegroundColor green
}
Write-Verbose "Ending SSHD setup process."
The code is a bit different than what you would run interactively. When you run Invoke-Command and specify a file, PowerShell treats the contents of the file as a scriptblock and runs it in a remote runspace. That is why the parameters are not typical function parameters. I'm passing the Verbose and WhatIf preferences from the original command to the script.
Invoke-Command -FilePath .\Setup-SSHServer.ps1 -ComputerName win10 -cred $artd -ArgumentList @("Continue", $True)
This gives me Verbose and Whatif output from the remote Windows 10 machine. Running the script with default values works just fine. If you want, you can add in the registry lines to set the default shell. But ultimately I want to configure for PowerShell remoting over ssh so I don't need them.
PowerShell 7 SSH Remoting
There's one last step to configure remoting to use ssh and that happens on the remote machine. You need to edit the sshd_config file that is found in $env:ProgramData\ssh. Here's my copy that has been edited with the necessary PowerShell requirements.
# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.
# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented. Uncommented options override the
# default value.
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
#HostKey __PROGRAMDATA__/ssh/ssh_host_rsa_key
#HostKey __PROGRAMDATA__/ssh/ssh_host_dsa_key
#HostKey __PROGRAMDATA__/ssh/ssh_host_ecdsa_key
#HostKey __PROGRAMDATA__/ssh/ssh_host_ed25519_key
# Ciphers and keying
#RekeyLimit default none
# Logging
#SyslogFacility AUTH
#LogLevel INFO
# Authentication:
#LoginGraceTime 2m
#PermitRootLogin prohibit-password
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10
#ENABLED
PubkeyAuthentication yes
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
# but this is overridden so installations will only check .ssh/authorized_keys
AuthorizedKeysFile .ssh/authorized_keys
#AuthorizedPrincipalsFile none
# For this to work you will also need host keys in %programData%/ssh/ssh_known_hosts
#HostbasedAuthentication no
# Change to yes if you don't trust ~/.ssh/known_hosts for
# HostbasedAuthentication
#IgnoreUserKnownHosts no
# Don't read the user's ~/.rhosts and ~/.shosts files
#IgnoreRhosts yes
# To disable tunneled clear text passwords, change to no here!
# ENABLED: Set this to YES for PowerShell remoting
PasswordAuthentication yes
#PermitEmptyPasswords no
#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
#PermitTTY yes
#PrintMotd yes
#PrintLastLog yes
#TCPKeepAlive yes
#UseLogin no
#PermitUserEnvironment no
#ClientAliveInterval 0
#ClientAliveCountMax 3
#UseDNS no
#PidFile /var/run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none
# no default banner path
#Banner none
# override default of no subsystems
Subsystem sftp sftp-server.exe
#ADDED
Subsystem powershell c:\progra~1\powershell\7\pwsh.exe -sshs -NoLogo
# Example of overriding settings on a per-user basis
#Match User anoncvs
# AllowTcpForwarding no
# PermitTTY no
# ForceCommand cvs server
Match Group administrators
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
You might need additional changes depending on your organization. I can use PowerShell to copy this file to the remote computer, assuming OpenSSH.Server and PowerShell 7 have been installed.
Copy-Item -Path .\sshd_config_default -Destination $env:ProgramData\ssh\sshd_config -ToSession $sess
The sshd service should be restarted for the changes to take effect.
Deployment
The last step is to pull all of this together into a master control script. I decided to add code to also install PowerShell 7 using my PSReleaseTools module.
#requires -version 5.1
#Deployment control script to setup SSH server on a remote Windows 10 desktop
#It is assumed PowerShell 7 is, or will be, installed.
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Computername,
[pscredential]$Credential,
[switch]$InstallPowerShell
)
#remove parameters from PSBoundparameter that don't apply to New-PSSession
if ($PSBoundParameters.ContainsKey("InstallPowerShell")) {
[void]($PSBoundParameters.remove("InstallPowerShell"))
}
if ($PSBoundParameters.ContainsKey("WhatIf")) {
[void]($PSBoundParameters.remove("WhatIf"))
}
#parameters for Write-Progress
$prog = @{
Activity = "Deploy SSH Server to Windows 10"
Status = $Computername.toUpper()
CurrentOperation = ""
PercentComplete = 0
}
#create the PSSession
Try {
Write-Verbose "Creating a PSSession to $Computername"
$prog.CurrentOperation = "Creating a temporary PSSession"
$prog.PercentComplete = 10
Write-Progress @prog
$sess = New-PSSession @PSBoundParameters -ErrorAction Stop
}
Catch {
Throw $_
}
if ($sess) {
if ($InstallPowerShell) {
$prog.currentOperation = "Installing PowerShell 7"
$prog.percentComplete = 25
Write-Progress @prog
#install PowerShell
if ($pscmdlet.ShouldProcess($Computername.toUpper(),"Install PowerShell 7")) {
Invoke-Command -ScriptBlock {
[void](Install-PackageProvider -Name nuget -force -forcebootstrap)
Install-Module PSReleaseTools -Force
Install-PowerShell -EnableRemoting -EnableContextMenu -mode Quiet
} -session $sess
} #whatif
}
#setup SSH
$prog.currentOperation = "Installing OpenSSH Server"
$prog.percentComplete = 50
Write-Progress @prog
Invoke-Command -FilePath .\Setup-SSHServer.ps1 -Session $sess -ArgumentList @($VerbosePreference, $WhatIfPreference)
#copy the sshd_config file. This assumes you've installed PowerShell 7 on the remote computer
Write-Verbose "Copying sshd_config to target"
$prog.currentOperation = "Copying default sshd_config to target"
$prog.percentcomplete = 60
Write-Progress @prog
Copy-Item -Path .\sshd_config_default -Destination $env:ProgramData\ssh\sshd_config -ToSession $sess
#restart the service
Write-Verbose "Restarting the sshd service on the target"
$prog.currentOperation = "Restarting the ssh service target"
$prog.percentComplete = 75
Write-Progress @prog
if ($pscmdlet.ShouldProcess("sshd","Restart service")) {
Invoke-Command { Restart-Service -Name sshd } -Session $sess
}
Write-Verbose "Removing the temporary PSSession"
$prog.currentOperation = "Removing temporary PSSession"
$prog.percentComplete = 90
Write-Progress @prog
$sess | Remove-PSSession
Write-Progress @prog -completed
$msg = @"
SSH Server deployment complete for $($Computername.toUpper()).
If PowerShell 7 is installed remotely, you should be able to test
with an expression like:
Enter-PSSession -hostname $Computername -username <user> -sshtransport
"@
Write-Host $msg -ForegroundColor yellow
} #if $sess
I should probably add better error handling around configuring the sshd service but this should set up everything so that I can do this:
Next Steps
The next part of my Windows journey with ssh and PowerShell is to get my Windows servers configured. Once that is done, I can use PowerShell remoting over ssh for just about everything. I'll dive into this tomorrow.
Jeff, what advantages do you see using ssh instead of winrm for PSRemoting on the windows platform, besides the obvious cross-platform unification?
Personally, I have no problem using WSMan. But there are companies that have never liked or trusted anything that comes from Microsoft and ssh is something they know and trust. Ultimately, cross-platform management is going to be the driving force.
On your server article, can you cover configuration of the Win2019 as an SFTP server. There are a few articles out there but everything defaults to the profile directory on the system drive for where the user logs in. I want to point the login elsewhere and I haven’t had much luck getting that working. It seems that chroot functionality isn’t really supported by Windows natively and it hasn’t been added to OpenSSH yet; unless I haven’t found a current enough article that talks about it.
There is a lot about configuring an ssh server that I am still learning. All I demonstrated was one way you might remotely install it on a Windows servers. Many companies have their own software and procedures for deploying software. My examples are for people who might need to build their own tooling. Or use it for personal lab testing like me. Installation is only half the story. How you configure it is up to you.
That is the problem with most of the articles out there. They don’t cover how to configure some of this stuff. I have read through the OpenSSH documentation (written from a Linux mindset) and the Windows addendum. None of them really cover the chroot setting value format. I am trying various things from different serverfault and other discussions to try to find something that works but I keep ending up in my C:\Users\username folder when I log in. Basically, it is making it unusable for an “in the wild” implementation. If I am just using it for internal stuff, I can probably make it work; but for any external application, I couldn’t use this.
I tend to avoid configuration topics because often that is going to be very specific to an organization. In your particular area of concern, it may be that this is a limitation of OpenSSH. But this is an open source project so you should at least be able to file an issue.
I’m trying out chrootdirectory. I think there may be some limitations for what you want to do in Windows. The Windows model doesn’t align with the way Linux works. What you are looking for is an ssh equivalent to a Just Enough Administration (JEA) endpoint in Windows PowerShell where you can limit what the connected user can see and do.
I found this which seems to address what you are trying to do. https://github.com/PowerShell/Win32-OpenSSH/issues/190
Yeah, that is one of the things I was looking at as well but so far, I can’t get it to work with any of the suggestions in there. I will keep banging at it and if I figure something out, I will let you know.