用SSH远程登录Windows

粒粒珍珠白玉光
釜中蒸煮饭飘香
三餐若少此君伴
腹内空空意未央
软硬适中口感好
搭配菜肴最是强
碳水精华凝一碗
人间至味是家常

本文介绍使用SSH登录Windows的方法,分为两种,一种是SSH到WSL2;另一种是到Windows。

本次使用的环境包括:

  • 至少一台Windows计算机,运行2022 datacenter, Win10和Win11应该也行,不过我没试过——没有。
  • 一台运行SSH客户端的计算机。

⚠️ 不合适的安全配置可能会使您的信息泄露。继续阅读代表您认可您对自己的行为负有全部责任。⚠️

C WSL2

到WSL2就不多说了,约等于一台Linux虚拟机,只不过藏在Windows后面。

安装

先打开一个管理员权限的PowerShell会话:

安装基本的支持组件

1
wsl --update

看看有哪些发行版(更多发行版可以自己去别的地方找)

1
wsl --list --online

安装一个发行版,默认是乌班图。可以自己加-d指定:

1
wsl --install # -d Debian

等一会就装好了,进去给它安装SSH服务:

1
2
3
sudo apt update
sudo apt install openssh-server
sudo nano /etc/ssh/sshd_config.d/99-my-sshd.conf

配置

以上配置文件有一点点安全加固,主要是不许密码验证,不许root登录:

1
2
3
4
5
6
Port 23456 # 自己挑个端口
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no
PermitRootLogin no
#PermitRootLogin prohibit-password

改好保存然后启动服务,这时不能使用systemctl

1
service ssh start

记下它的IP地址:

1
ip addr

网络

回到那个管理员权限的PowerShell会话。

加入一个端口映射,令Windows将来自外部的TCP23456映射为WSL2的TCP23456:

1
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=23456 connectaddress=<WSL2 IP地址> connectport=23456

建好之后可以看看:

1
netsh interface portproxy show v4tov4

以后不想要了可以删掉:

1
netsh int portproxy reset all

防火墙

还得创建Windows防火墙规则,这步在管理员权限的PowerShell会话完成:

1
netsh advfirewall firewall add rule name=Allow TCP 23456 for WSL2 dir=in action=allow protocol=TCP localport=23456

建好之后可以看看:

1
netsh advfirewall firewall show rule name=all dir=in type=dynamic | select-string WSL2

不想要了可以删掉:

1
netsh advfirewall firewall delete rule name="Open Port 23456 for WSL2"

systemd

这步可选,主要是方便以后搞服务。

先写一个配置文件,就两行。

1
2
echo '[boot]
systemd=true' | sudo tee /etc/wsl.conf

然后回到管理员的PowerShell重启这个发行版:

1
wsl --shutdown

重启之后就可以用systemctl

1
systemctl list-unit-files --type=service

SSH公钥

为了照顾小白,还是说下密钥是怎么来的。 在自己常用的计算机上面生成

1
ssh-keygen -t ed25519 -C "备注"

生成的pub文件放在SSH服务器上。 另一个当成密码,别给任何人。

至于复制到SSH服务器上你们有别的方法就用哈,没有的话就像我这样土法制作。这步在WSL2的窗口完成:

1
2
mkdir ~/.ssh
nano ~/.ssh/authorized_keys

以上把公钥粘贴进去,每行一个。

1
chmod 600 ~/.ssh/authorized_keys

好了,可以用SSH客户端登录了。

一个~/.ssh/config 的例子:

1
2
3
4
5
6
Host foo-deb # 昵称
    ForwardAgent yes
    HostName foo.bar.com # 有FQDN就填,没有就写IP地址
    User <username> # WSL2里的用户名
    Port 23456 # 之前选的端口
    RequestTTY yes

在常用的计算机上用 ssh foo-deb即可登录。

玩腻了可以:

1
wsl --uninstall

D 正片开始

以上是水字数的,其实是SSH到Linux,接下来才是SSH到Windows。

先开个管理员权限的PowerShell会话。

安装个SSH服务:

1
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

这个安装好之后名字就叫“sshd”,如图: sshd service for Windows

设为开机启动(管理员权限的PowerShell会话):

1
Set-Service -Name sshd -StartupType 'Automatic'

给它新建防火墙规则(管理员权限的PowerShell会话):

1
2
3
4
5
6
if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
    Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
    New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
} else {
    Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
}

编辑$env:ProgramData\ssh\sshd_config这个文件,找到下面这两行,把它们注释掉,去掉对于管理员组成员的设置,它这里检测到是管理员组成员就去第2行的地方找key:

1
2
Match Group administrators
   AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

用脚本的话是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$filePath = "C:\ProgramData\ssh\sshd_config"
$content = Get-Content $filePath
$newContent = @()
foreach($line in $content) {
    if ($line -notmatch "Match Group administrators.*" -and $line -notmatch "AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys") {
        $newContent += $line
    } else {
        $newContent += "#" + $line
    }
}
$newContent | Set-Content $filePath

启动!(管理员权限的PowerShell会话)

1
Start-Service sshd

SSH key当然是首选的验证方式,请准备好公钥。文件的位置是$env:USERPROFILE\.ssh\authorized_keys。需要手动将权限设置为只有自己能访问(阻止继承)。资源管理器的图形界面不多说了。要想使用脚本的话把下列内容存为importKey.ps1文件,在一个非管理员PowerShell会话运行之(importKey.ps1),粘贴公钥到剪贴板,每行一个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Write-Warning "请先将公钥(pub)复制到剪贴板,然后按任意键继续。" -WarningAction Inquire

# 拿到当前用户名,格式为domain\username
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
# 读取剪贴板内容
$clipboardContent = Get-Clipboard
# 准备写入的文件路径
$filePath = "$env:USERPROFILE\.ssh\authorized_keys"
$sshFolderPath = "$env:USERPROFILE\.ssh"
if (-not (Test-Path $sshFolderPath)) {
    New-Item -Path $sshFolderPath -ItemType Directory
}
Write-Host "剪贴板内容如下:"
Write-Host $clipboardContent
Write-Host "确认要将此内容写入 $filePath 吗?(Y/N)"
$confirmation = Read-Host
if ($confirmation -eq 'Y') {
    # 写入公钥,阻止文件权限继承,为自己授权
    Set-Content -Path $filePath -Value $clipboardContent
    # 确保 $user 正确处理,没有额外的引号
    $escapedUser = $user.Replace("\", "\\")  # 替换斜杠
    # 使用 ${} 来处理变量引用
    icacls.exe $filePath /inheritance:r /grant "${escapedUser}:(F)" /grant "SYSTEM:(F)"
    Write-Host "内容已写入文件。"
} else {
    Write-Host "操作取消。"
}

这样就可以用了。 ssh <FQDN | IP>即可。不过默认的shell是cmd ,要想改成SSH进来默认是PowerShell的话那就得改下注册表(管理员权限的PowerShell会话):

1
2
3
4
5
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" `
                 -Name DefaultShell `
                 -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
                 -PropertyType String `
                 -Force

改好之后再SSH进去默认就是PowerShell了。

㊟ 有的终端仿真App会“偷偷”加些料在SSH会话里面,实现git状态、语法高亮、自动补全、纠正等等功能。 但它既不认识cmd也不认识PowerShell,于是无法登录。各位要是使用这种App就得注意一下。图为我用的warp终端。使用这种终端时我加入了如图的设置,并在欲登录到Windows时使用command ssh命令代替ssh命令。

Warp terminal subshell

E PowerShell over SSH

以上通过SSH到Windows然后运行PowerShell很好玩,但是它有个问题,就是用法必须是交互式的。还有一种玩法,能把PowerShell会话包装在SSH里面,这样我们就可以编写能循环运行的脚本。

想象下,在一台Windows计算机通过PowerShell远程处理另一台Windows计算机的时候,它使用的是WinRM, 身份验证则是Kerberos或者NTLM。

比如有人想获取一堆Windows计算机的某服务的状态,我们的循环脚本可以这样:

1
Get-Service $servicename -ComputerName $server

但要是有人想从macOS/Linux做同样的事情呢?多么蛋疼 ;又或者就是从Windows到Windows但是网络团队把TCP5985/5986端口封了您恰好发现他们忘记封TCP22,答案也是可以的。 不过Windows自带的Powershell不行。没这个能力知道吗

1
2
3
4
5
6
7
8
(Get-Command New-PSSession).ParameterSets.Name
---
ComputerName
Uri
VMId
VMName
Session
ContainerId

Windows端得装新的,官方链接点开就装。 同样,客户端也得装,比如macOS:

1
brew install powershell/tap/powershell-lts

或者乌班图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
sudo apt update
sudo apt install -y wget apt-transport-https software-properties-common
# 乌班图版本
source /etc/os-release
# 下载微软仓库的key
wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb
# 注册微软仓库的key
sudo dpkg -i packages-microsoft-prod.deb
# 文件清理
rm packages-microsoft-prod.deb
# 更新仓库列表
sudo apt update
# 正活
sudo apt install -y powershell
# 启动!
pwsh

目前版本是7,装完了是这样的(下面倒数两行带来的能力):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
PS C:\Program Files\PowerShell\7> (Get-Command New-PSSession).ParameterSets.Name
ComputerName
Uri
VMId
VMName
Session
ContainerId
UseWindowsPowerShellParameterSet
SSHHost
SSHHostHashParam

默认Shell也改成新的(可选):

1
2
3
4
5
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" `
                 -Name DefaultShell `
                 -Value "C:\Program Files\PowerShell\7\pwsh.exe" `
                 -PropertyType String `
                 -Force

还是编辑$env:ProgramData\ssh\sshd_config这个文件,把它本来的Subsystem 的行注释掉。换成这样的:

1
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -nologo

以上不支持文件路径有空格。所以得用老DOS的风格,您要是不确定您的老DOS风格路径是什么,可以这样找:

1
2
3
4
5
6
7
# 找到位置
$path = where.exe pwsh.exe
"Long path: $path"

# 转为8.3风格
$shortPath = (New-Object -ComObject Scripting.FileSystemObject).GetFile($path).ShortPath
"Short path: $shortPath"

改好了之后重启服务:

1
Restart-Service sshd

然后就可以试试了,比如交互式的:

1
2
Enter-PSSession -HostName <FQDN | IP>
[<FQDN | IP>]: PS C:\Users\username\Documents> Exit-PSSession

也可以尝试批量的:

1
2
$session = New-PSSession -HostName $server
Invoke-Command $session -ScriptBlock { Get-service sshd | Stop-Service }

完整版,想好了再运行哦😿:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$serversFile = "list.txt"
$servers = Get-Content $serversFile

foreach ($server in $servers) {
    try {
        $session = New-PSSession -HostName $server
        Invoke-Command $session -ScriptBlock { Get-service sshd | Stop-Service }
        Write-Host "Stopped sshd service on $server successfully."
    } catch {
        Write-Host "Error occurred while processing $server: $_"
    } finally {
        if ($session) {
            Exit-PSSession $session
        }
    }
}

㊟ 通过SSH远程执行PowerShell是不带$PROFILE(配置文件,不是SSH的)的,我觉得这是好事——省时间;特别想要的话可以自己运行.$profile 命令。


我们学习了用SSH登录Windows PowerShell的方法,这个方法也能用于SSH到macOS/Linux的PowerShell上面,蛋疼的喜欢的可以试试。

好了,看过了就等于会了。感谢观看!😺

0%