Win32 OpenSSH Package
Update 20/09/2018
Updated the script to UseBasicParsing so it works on Server core out of the box. Also, if you have to allow the port on Windows Firewall:
New-NetFirewallRule -DisplayName "Allow SSH" -Direction Inbound -LocalPort 22 -Protocol TCP -Action Allow
Update 11/09/2018
I've made a copy of this script which downloads the dependencies (including PSCore. Also of note, on a machine I ran it on, I had to set the allowed .Net TLS modes before it would let me download from github.
[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
I've recently been using Macos at work in order to share admin responsibilities across the team. Still suppporting Windows, however there are a couple of tools I use to make working Windows from a Mac simpler.
PowerShell
Microsoft Open-Sourced PowerShell in 2016 and today in 2018, you can get stable installations for Macos, Linux and Windows on github which is often referred to as PSCore.
As an aside, this new version of powershell is not nativly backward compatible with compiled binary modules of the previous "Windows Powershell", however recently in development is a new module: WindowsCompatibility (currently only available on Windows Insider builds) that allows your to import "Windows Powershell" modules into PSCore.
When alpha and beta builds first became available I started testing remote sessions from Linux and Macos to Windows (As I would prefer to work from a unix system at work), but quickly found that the native "Enter-PSSession" wasn't supported from PSCore.
OpenSSH
Around the same time, Microsoft began working with the OpenBSD's OpenSSH project to bring official OpenSSH builds to Windows and the PSCore team found a way to make "Enter-PSSession" work with this.
Packaging PSCore
Packaging PSCore is very straightforward and I won't go into detail here. Suffice it to say that PSCore is released as an MSI and these are very simple to deploy using tools like Configuration Manager.
OpenSSH Package
Essentially I created a Windows Powershell script which follows the installation directions on the Win32 OpenSSH github Installation page.
The Scripts
Deploying an application using scripts in Configuration Manager, usually requires 3 scripts, and this case is no exception. I have provided the all needed scripts below:
Install.ps1
[CmdLetBinding()]
Param()
#region Helper functions
function Get-Path {
[CmdLetBinding()]
Param(
[ValidateSet(
"Machine",
"User"
)]$Context = "User",
[Switch]$Raw
)
If ($Context -eq "Machine") {
$Root = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
} else {
$Root = 'HKCU:'
}
If ($Raw){
Get-ItemPropertyValue -Path "$Root\Environment" -Name Path
} Else {
Try {
(Get-ItemPropertyValue -Path "$Root\Environment" `
-Name Path -EA SilentlyContinue) -split ';'
} Catch {
Write-Warning "No user environment variables found"
}
}
}
function Add-Path {
[CmdLetBinding()]
Param(
[Parameter(Mandatory=$True)]
[ValidateScript({
if (Test-Path -Path $_) {
$True
} else {
throw "Unable to validate path $_"
}
})]$Path,
[ValidateSet(
"Machine",
"User"
)]$Context
)
Write-Verbose "Adding $Path to environment"
if ($Context -eq 'Machine') {
If (! $Path -in (Get-Path -Context Machine)){
Write-Verbose "Adding $Path to machine context"
setx /m PATH "$(Get-Path -Context Machine -Raw);$Path"
}
} else {
Write-Verbose "Adding $Path to user context"
If (! $Path -in (Get-Path -Context User)){
Write-Verbose "Adding $Path to user context"
setx PATH "$(Get-Path -Context Use -Raw);$Path"
}
}
}
function New-SymbolicLink {
Param($Link,$Target)
If (-Not (Test-Path -Path $Link)){
If ((Get-Item $Target).PSIsContainer) {
cmd.exe /c mklink /D $Link $Target
} Else {
cmd.exe /c mklink $Link $Target
}
}
}
#endregion
# Extract OpenSSH
$Archive = Get-ChildItem -Filter *.zip
Expand-Archive -Path $Archive -DestinationPath $env:ProgramFiles
Rename-Item -Path $Env:ProgramFiles\OpenSSH-Win64 -NewName OpenSSH
#Add InstallDir to Path
Add-Path -Path $Env:ProgramFiles\OpenSSH -Context Machine -Verbose
# Configure OpenSSH
& $Env:ProgramFiles\OpenSSH\install-sshd.ps1
# Start sshd service
Start-Service -Name sshd
# Set service startup
Set-Service sshd -StartupType Automatic
Set-Service ssh-agent -StartupType Automatic
# Setup pwsh link to work around
# https://github.com/PowerShell/Win32-OpenSSH/issues/784
# Find PSCore Install and Make symbolic link
$PSCoreDir = Get-ChildItem -Path $env:ProgramFiles\PowerShell `
-Directory | Select-Object -Last 1
New-SymbolicLink -Link $env:SystemDrive\pwsh -Target $PSCoreDir.FullName
# Enable Password Authentication and set pwsh as default shell
$NewConfig = Get-Content -Path $Env:ProgramData\ssh\sshd_config |
ForEach-Object {
Switch ($_) {
{$_ -match '^#PasswordAuthentication\syes'} {$_.replace('#','')}
{$_ -match '^#PubkeyAuthentication\syes'} {$_.replace('#','')}
{$_ -match '^Subsystem\s+sftp\s+'} {
'Subsystem powershell c:\pwsh\pwsh.exe -sshs -NoLogo -NoProfile'
}
Default {$_}
}
}
# Update sshd config
Set-Content -Path $Env:ProgramData\ssh\sshd_config -Value $NewConfig `
-Force
# Restart sshd
Restart-Service sshd
Uninstall.ps1
[CmdLetBinding()]
Param()
#region Helper functions
function Remove-SymbolicLink {
Param($Link,$Target)
If (Test-Path -Path $Link){
If ((Get-Item $Target).PSIsContainer) {
cmd.exe /c rmdir $Link
} Else {
cmd.exe /c del $Link
}
}
}
function Get-Path {
[CmdLetBinding()]
Param(
[ValidateSet(
"Machine",
"User"
)]$Context = "User",
[Switch]$Raw
)
If ($Context -eq "Machine") {
$Root = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
} else {
$Root = 'HKCU:'
}
If ($Raw){
Get-ItemPropertyValue -Path "$Root\Environment" -Name Path
} Else {
Try {
(Get-ItemPropertyValue -Path "$Root\Environment" `
-Name Path -EA SilentlyContinue) -split ';'
} Catch {
Write-Warning "No user environment variables found"
}
}
}
function Remove-Path {
[CmdLetBinding()]
Param(
[Parameter(Mandatory=$True)]
$Path,
[ValidateSet(
"Machine",
"User"
)]$Context
)
Write-Verbose "Removing $Path from environment"
if ($Context -eq 'Machine') {
If ($Path -in (Get-Path -Context Machine)){
Write-Verbose "Removing $Path from machine context"
$NewPath = ""
Get-Path -Context Machine | Where-Object {
$psItem -ne $Path -and
$psItem -ne ""
} ForEach-Object {
$NewPath += "$psItem;"
}
setx /m PATH "$NewPath"
}
} else {
Write-Verbose "Removing $Path from user context"
If ($Path -in (Get-Path -Context User)){
Write-Verbose "Removing $Path from user context"
$NewPath = ""
Get-Path -Context User | Where-Object {
$psItem -ne $Path -and
$psItem -ne ""
} ForEach-Object {
$NewPath += "$psItem;"
}
setx PATH "$NewPath"
}
}
}
#endregion
& $Env:ProgramFiles\OpenSSH\uninstall-sshd.ps1
# Extract OpenSSH
Remove-Item -Path $env:ProgramFiles\OpenSSH -Recurse -Force
Remove-Path -Path $env:ProgramFiles\OpenSSH -Context Machine -Verbose
# Find PSCore Install and remove symbolic link
$PSCoreDir = Get-ChildItem -Path $env:ProgramFiles\PowerShell -Directory | Select-Object -Last 1
Remove-SymbolicLink -Link $env:SystemDrive\pwsh -Target $PSCoreDir.FullName
# Remove old config
Remove-Item -Path $env:ProgramData\ssh -Recurse -Force
Detect.ps1
$AssumeInstalled = $True
If (-Not (Test-Path $Env:ProgramFiles\OpenSSH)) {
$AssumeInstalled = $False
}
If (-Not (Test-Path $Env:SystemDrive\pwsh)) {
$AssumeInstalled = $False
}
If (-Not (Get-Service sshd -ErrorAction SilentlyContinue)) {
$AssumeInstalled = $False
}
If ($AssumeInstalled) {
Write-Output "True"
}
Using OpenSSH with Powershell
Now that I have used these scripts to deploy OpenSSH and PSCore, I can PSRemote to a PC using my Mac.
The old way to use "Enter-PSSession" was by specifying the ComputerName parameter like so:
PS\> Enter-PSSession -ComputerName Blah
However, when using OpenSSH with a PS Session you do the following:
PS\> Enter-PSSession -HostName Blah -UserName MrBlah
You could also setup a session in a variable and resue it multiple times in a session:
PS\> $s = New-PSSesssion -HostName Blah -UserName MrBlah
PS\> Enter-PSSession -Session $s
[Blah] PS\>
I hope you have found this usefull.
Tags: powershell, pscore, openssh, windows, macos