2026年8月18日
Windows 安装 SSH Server(OpenSSH)指南:Win11/10 用内置功能,Win8.1/7 手动部署
远程桌面给你的是一块屏幕,SSH 给你的是一条通道。这个区别正是”为什么要在 Windows 上跑 SSH 服务器”的答案:用 scp 把构建产物传到测试机,不用先共享文件夹;在一台无头的 CI 机器上 tail 日志,不用远程桌面连过去;用一个 shell 脚本对二十台 Windows 主机执行部署;或者给同事开一台共享工作站的访问权限,但不必给他一个交互式桌面会话。从 Windows 10 1809(2018 年 10 月更新)开始,微软把真正的 OpenSSH 服务器作为可选功能内置进了系统——就是 PowerShell 团队维护的那套 Win32-OpenSSH 代码。曾经要花一下午折腾 Cygwin 的活儿,从此变成了两条命令的事。
麻烦在于”Windows”这个词横跨了好几个时代,而 1809 这条线两侧的安装路径完全不同。Windows 11 和 Windows 10 1809+ 上,OpenSSH Server 是一个”按需功能”(Features on Demand),在设置界面点几下、或者一条 PowerShell 命令就能启用;Windows 10 旧版本、Windows 8.1 和 Windows 7 上什么都没内置,你要么手动安装 Win32-OpenSSH 的 GitHub 发布包,要么用 Cygwin。而配置层——sshd_config、密钥认证、大名鼎鼎的 administrators_authorized_keys 规则——在所有版本上完全一致,真正的排障也大都发生在这一层。
这篇文章按这个结构展开:先按版本讲安装,再讲对所有安装方式都适用的配置与加固,最后给一份你实际会遇到的故障排查手册。
Windows 11 与 Windows 10(1809 及以后):直接用内置功能#
用 winver 确认版本号在 1809 及以上,内置可选功能就是唯一值得考虑的路径。它随 Windows Update 一起更新,服务在服务控制管理器里和其他服务平起平坐,以后想卸载也只是一条命令的事。Windows Server 2019 及以后同样带这个功能,所以本节内容对服务器装机一样适用。
先看看装了什么#
OpenSSH 客户端往往已经就位,服务器几乎从来不在。用管理员 PowerShell 一次查清:
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'
你会看到两行:OpenSSH.Client~~~~0.0.1.0 和 OpenSSH.Server~~~~0.0.1.0,各带一个 State,值为 Installed 或 NotPresent。注意只有客户端是不够的——客户端是往外连的 ssh 命令,真正监听入站连接的是服务器功能。
从”设置”安装(图形界面路径)#
打开设置 → 应用 → 可选功能(Windows 11 上就是”应用 → 可选功能”;老一点的 Windows 10 版本在”应用和功能 → 可选功能”里),点添加功能或查看功能,在搜索框输入 “OpenSSH”,选OpenSSH 服务器——别选成”OpenSSH 客户端”。点下一步 → 安装。功能包从 Windows Update 下载,一两分钟装完。唯一例外的场景是完全离线的机器:那需要按需功能 ISO 做源,此时即使在 Windows 11 上,走下文的 Win32-OpenSSH 手动安装反而更省事。
用 PowerShell 安装(可复现的路径)#
脚本化的等价操作,凡是打算做第二次的事都该用它:
# 需要管理员 PowerShell
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
那四个波浪线是功能名的一部分,不是笔误——字符串就是 OpenSSH.Server~~~~0.0.1.0,一个字符都不能少。装完输出里 Online : True、RestartNeeded : False,不需要重启。
启动服务并设为开机自启#
装上功能只是注册了 sshd 服务,它既没启动、启动类型也是手动——这是有意为之,SSH 监听应该是你主动打开的东西,而不是”顺便就发生了”的东西。两条命令补齐:
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
首次启动时,sshd 会在 C:\ProgramData\ssh\ 下生成主机密钥(ssh_host_ed25519_key、ssh_host_rsa_key 等)。自动防火墙规则也是在这一刻出现的,这和下一步直接相关。如果这台机器还要作为客户端使用带口令保护的密钥,顺手把 ssh-agent 服务也开了——加固一节细说。
确认防火墙规则#
功能安装会创建一条名为 OpenSSH-Server-In-TCP 的入站规则,在所有配置文件上放行 TCP 22。确认它存在且已启用:
Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP"
如果它不在——最常见于做过安全基线加固的镜像,或被组策略清掉了规则——手动补一条:
New-NetFirewallRule -Name sshd -DisplayName "OpenSSH Server (sshd)" `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
这一层有两件事值得知道。第一,如果之后在 sshd_config 里改了端口,这条规则不会跟着你走——新端口要么补一条新规则,要么改 LocalPort。“改了端口就连不上了”,头号原因就是这个两步没做齐。第二,Windows 防火墙可能只是好几道墙中的一道:企业终端往往还跑着主机安全Agent,它有自己的策略;“规则明明在、Agent 悄悄拦”是真实存在的故障模式,症状和没有规则一模一样。
第一次连接#
在服务器上先拿两个信息:用 whoami 确认用户名(输出形如 COMPUTERNAME\alice,反斜杠后面那段就是客户端要敲的名字——Windows 账户名里带空格是完全合法的,也是首次连接翻车的稳定来源),用 ipconfig 拿 IPv4 地址。然后在另一台机器上:
ssh [email protected]
会提示输入 alice 的 Windows 密码,并要求确认主机密钥指纹。别无脑输 yes:先把服务器展示的指纹和服务器自己记录的比对一下:
# 在服务器上执行
ssh-keygen -lf C:\ProgramData\ssh\ssh_host_ed25519_key.pub
一致再接受。落进去之后是一个 cmd.exe 提示符,显示 Microsoft Windows [Version ...]——这就是默认 shell,多数人装完第一件事就是把它换掉(下文讲)。敲一条命令证明会话真的是远程那台机器:
whoami
hostname
往下走之前还有一个账户模型提醒:如果账户用的是微软账户而不是本地账户登录,SSH 密码就是微软账户的密码,但用户名仍然是 whoami 显示的本地配置文件名。这个组合坑过的人不计其数。对要长期走 SSH 管理的机器,建一个专用本地账户是更省心的底座。
Windows 10 旧版本(1809 之前)/ Windows 8.1 / Windows 7#
这些系统早于按需功能机制,什么都不内置。现实的选择有两条,怎么选主要看机器上是否已经有一套类 POSIX 环境。
方案 A:Win32-OpenSSH 的 GitHub 发布包#
这就是微软后来收进 Windows 的那个项目,打包成 zip 供手动安装。Windows 7 SP1 到当前所有版本都能用——不过要注意新近版本只针对现代 Windows,Win7 的机器应该用同一发布页里的老版本,旧 tag 一直保留着下载,正是为这种场景。
完整步骤,全部在管理员 PowerShell 里执行:
-
从 Win32-OpenSSH 的 GitHub releases 页面下载 OpenSSH-Win64.zip(32 位系统用 Win32 版),解压前先解除阻塞——从网上下载的文件带着 Mark-of-the-Web 区域标记,PowerShell 会因此拒绝执行包里的脚本:
Unblock-File .\OpenSSH-Win64.zip Expand-Archive .\OpenSSH-Win64.zip -DestinationPath "$env:ProgramFiles"安装脚本认定的位置是
C:\Program Files\OpenSSH,所以如果解压出来的是C:\Program Files\OpenSSH-Win64,把这个文件夹改名为C:\Program Files\OpenSSH。 -
安装并注册服务:
powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\OpenSSH\install-sshd.ps1"这一步会创建
sshd和ssh-agent两个服务,在C:\ProgramData\ssh\下生成主机密钥,并写出默认的sshd_config。如果全新 Win7 上报缺 DLL,元凶通常是 Visual C++ 运行库——装上对应架构的 redistributable 再跑一遍。 -
开防火墙:
New-NetFirewallRule -Name sshd -DisplayName "OpenSSH Server (sshd)" ` -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22Windows 7 上没有
New-NetFirewallRule,用netsh:netsh advfirewall firewall add rule name="OpenSSH Server (sshd)" dir=in action=allow protocol=TCP localport=22 -
启动服务并设为开机自启:
Set-Service sshd -StartupType Automatic Start-Service sshd
以后要卸载,Stop-Service sshd 之后在同一目录跑 uninstall-sshd.ps1 即可,服务会被干净地注销——这也是方案 A 优于自己手搓服务的很大一部分理由。
方案 B:Cygwin 或 MSYS2 的 OpenSSH#
如果这台机器本来就在用 Cygwin——老的构建机很常见——在现有环境里装 OpenSSH 可以少养一套工具链。Cygwin 的包管理器里有 openssh,配套的 ssh-host-config 脚本负责 Windows 服务的接线,用的是 Cygwin 自己的服务运行器。代价是结构性的:Cygwin 的 sshd 对接的是它自己那套映射到 Windows 账户的模拟 POSIX 账户库;路径翻译(/cygdrive/c 对 C:\)会渗进每个会话;而且 Cygwin 这一层本身的维护也归你了。MSYS2 也能通过 pacman -S openssh 装 sshd,但它没有能和 ssh-host-config 相提并论的服务框架,最后免不了自己拿计划任务或包装脚本凑——可行,但除非 MSYS2 已经是这台机器的中心,否则不值得。
对一台”充当可 SSH 访问的 Windows 机器”来说,方案 A 是老实的默认选项;方案 B 是给本来就活在那些环境里的机器准备的。
配置与安全加固:各版本通用#
下面所有内容都在改 C:\ProgramData\ssh\sshd_config——不管你是走内置功能还是 GitHub zip 装的,文件都在同一个地方。每改一次都要重启服务:Restart-Service sshd。测试改动时务必留着第二个会话:经典的自己坑自己,是在唯一的 SSH 会话里改配置,然后带着一个语法错误重启了 sshd。
sshd_config 三个关键项#
默认配置保守而合理。多数装机最终会碰的是这三行:
Port 22
PasswordAuthentication yes
PubkeyAuthentication yes
- Port:改端口属于”以隐蔽换安全”,但有一点真实收益——互联网上的无差别扫描器从此与你无关,面向公网的主机日志噪音会明显下降。改了端口就必须同步改前面那条防火墙规则,这个两步没做齐,是”改完端口连不上”的头号成因。
- PasswordAuthentication no:单条收益最大的加固项,但前提是密钥登录已经验证可用。在还能用密码自救的时候关掉它,而不是之前。顺带说一句,就算 SSH 被锁在门外,有本地控制台或 RDP 的机器都救得回来——烦人是真的,致命倒不至于,除非那是一台远端的无头机器。
- PubkeyAuthentication yes 默认就是开的,平时不用动,但知道它在哪一行,排障的时候有用。
在客户端生成密钥#
任何 Windows 10 1809+ 或 Windows 11 机器都内置 OpenSSH 客户端,ssh-keygen 直接可用——不需要再装 PuTTYgen。生成一把 Ed25519 密钥,这是现代场景下的默认选择(密钥短、签名快、也没有”该用多长”的争论):
ssh-keygen -t ed25519 -C "alice@laptop"
位置用默认的(C:\Users\alice\.ssh\id_ed25519),口令要设——没有口令的私钥就是一张不记名凭证,会跟着备份和同步文件夹到处旅行。产出是一对文件:私钥 id_ed25519 永远不离开客户端;公钥 id_ed25519.pub 只有一行内容,你要把它安装到服务器上。客户端这边顺手启用 ssh-agent,口令就按”每次开机问一次”而不是”每次连接问一次”来收:
Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add
administrators_authorized_keys 规则——配密钥之前先读这段#
这个坑消耗的排障时间,超过 Windows SSH 其他所有话题的总和。在 Linux 上,公钥放进 ~/.ssh/authorized_keys 就完事了。Windows 上这里有一个分叉:
- 如果账户是标准用户,公钥放
C:\Users\<user>\.ssh\authorized_keys,和你想的一样。 - 如果账户属于 Administrators 组,sshd 会无视那个文件,改读
C:\ProgramData\ssh\administrators_authorized_keys。
默认 sshd_config 的结尾有一段实现的正是这件事:
Match Group administrators
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
为什么要有这个分叉,值得弄明白——因为它解释了接下来那套 ACL 体力活。授予管理员级登录权限的密钥被当作”全机配置”而不是”用户私有数据”对待:C:\ProgramData\ssh 只有 Administrators 和 SYSTEM 可写,所以一个以低权限账户运行的进程没法往里塞公钥、再悄悄给自己登记一个管理员级 SSH 入口;而用户配置文件目录的 ACL 跟着用户本人走。POSIX 上这件事靠文件属主检查来把关,Windows 的 ACL 模型在”这个文件属于这个用户”的映射上正好有不兼容的地方,把管理员密钥集中到一个受控目录就绕开了整个问题。你可以删掉那段 Match 强制所有账户都走用户文件,但更聪明的做法是顺着设计走。
于是:管理员账户的公钥要写进机器级文件,把 id_ed25519.pub 里那一行粘贴进去:
notepad C:\ProgramData\ssh\administrators_authorized_keys
然后——所有人都会漏掉的一步——修 ACL。这个文件继承了 ProgramData 相当宽松的权限继承,而 sshd 拒绝从”Administrators 和 SYSTEM 之外的组也能写”的文件里读密钥。这个拒绝对客户端来说是静默的:你的密钥就是不生效,如果密码登录还开着,就回落到密码。标准修法:
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /grant "SYSTEM:(F)"
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /grant "BUILTIN\Administrators:(F)"
/inheritance:r 把继承来的 ACE 全部移除——那个 r 是 remove(移除),不是 replace(替换)——留下一个只有 SYSTEM 和内置 Administrators 组可访问的文件。验证一下:
icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys"
输出里应该只有 NT AUTHORITY\SYSTEM 和 BUILTIN\Administrators,都是 (F)。多出任何一条,管理员账户的密钥认证就会继续安静地失败。
标准用户是同一件事的缩小版:C:\Users\<user>\.ssh\authorized_keys 同样会在 ACL 过松时失效——从别的机器拷过来之后尤其常见。照样收紧:
icacls.exe "$env:USERPROFILE\.ssh\authorized_keys" /inheritance:r /grant "${env:USERNAME}:(F)" /grant "SYSTEM:(F)"
同一个片区还有两个小坑。其一,这个文件必须是纯文本——老版记事本的默认”Unicode”编码(UTF-16)会让密钥读不出来;存成纯文本或无 BOM 的 UTF-8,或者干脆用 PowerShell 的 Set-Content/Add-Content 写文件,它默认就是对的。其二,Windows 上没有 ssh-copy-id,而 ssh user@host "cat >> ..." 这种管道的结局取决于服务器默认 shell 是什么——cmd.exe 里根本没有 cat。往记事本里粘贴朴素无华,但永远有效;对一次性的配置步骤来说,“朴素且永远有效”就是正确的取舍。
把默认 shell 换成 PowerShell#
默认情况下 SSH 会话落进的是 cmd.exe——一个从 1988 年干到今天的 shell。把 sshd 指向 PowerShell——注意这是注册表设置,不是 sshd_config 里的,第一次见的人都会愣一下:
if (-not (Test-Path "HKLM:\SOFTWARE\OpenSSH")) {
New-Item -Path "HKLM:\SOFTWARE\OpenSSH" -Force
}
# 装了 PowerShell 7+ 的话
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value "C:\Program Files\PowerShell\7\pwsh.exe" -PropertyType String -Force
# ...或者用内置的 Windows PowerShell 5.1
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
新会话立刻生效。这一处改动对日常体验是脱胎换骨的——远程管理从解析 cmd 文本输出,变成走 SSH 的对象管道;而且 VS Code 的 Remote-SSH 扩展在目标是 Windows 时,期待的也正是一个像样的 shell。
排障:你实际会遇到的故障#
”连接被拒”或超时#
从服务本身开始往外查:
Get-Service sshd # 必须是 Running
Get-NetTCPConnection -LocalPort 22 -State Listen # 必须有东西在监听
没有监听,说明服务没起来或配置没解析过——去看事件日志(下文)。有监听,就在服务器本机自测:ssh localhost。本机通、远程不通?问题在两台机器之间:防火墙规则缺失或被禁用、主机安全Agent、VPN 分流路由,或者干脆是 IP 找错了(多网卡的机器——Wi-Fi 加有线——只在其中一块上应答,ipconfig 两块都显示,确认你瞄准的是同网段那块)。本地测试通过之后,才轮到怀疑网络本身。
密钥认证静默失败#
服务器收了你的密码,但无视了你的密钥。按命中概率排序:你的账户在 Administrators 组里,密钥放错了文件(就是上文那个 Match Group administrators 分叉——用 whoami /groups 确认);文件位置对但 ACL 太松(跑一遍 icacls 命令,核对输出里只剩允许的主体);公钥那一行被编辑器折了行或转了编码;或者你被问的其实是密钥的口令,而你读成了密码提示。sshd 有意不把这些告诉客户端——认证失败在线上协议里故意写得含糊其辞——这正是下一个工具存在的理由。
sshd -d:一次一个连接的实时日志#
服务往事件日志里写东西,但信噪比最高的诊断手段是把守护进程以前台调试模式跑起来,让它把每一次认证决策现场播报出来:
Stop-Service sshd
& "C:\Windows\System32\OpenSSH\sshd.exe" -d
(GitHub 手动安装的路径是 C:\Program Files\OpenSSH\sshd.exe。)现在从客户端发起连接,盯着屏幕:你会看到它评估你递交的密钥;如果 ACL 不对,会有一行类似 Authentication refused: bad ownership or modes for file ...——哪个文件、什么原因,一目了然。-d 模式只服务一个连接就退出;再加一个 d(-dd)更啰嗦,加 -p 2222 则在备用端口上监听,方便在不动生产监听的情况下测试配置改动。弄完 Ctrl+C,然后 Start-Service sshd。
事件日志#
服务正常运行期间发生的一切,看事件查看器(eventvwr.msc)→ 应用程序和服务日志 → OpenSSH → Operational。服务启动失败——最经典的就是 sshd_config 里一行写错——会连着行号出现在这里,这比一个闷声不响起不来的服务肯说的多得多。
FAQ#
WSL 里的 sshd 和 Windows 的 sshd 冲突吗?#
取决于 WSL 版本,因为它们的组网方式不同。WSL2 在一个轻量虚拟机里跑真正的 Linux 内核,有自己的 NAT 网络栈——它的 22 端口和 Windows 的 22 端口在不同的网络接口上,两个同时监听相安无事;要从外部访问 WSL 那个,需要额外做端口转发。WSL1 直接共享 Windows 网络栈,两个 sshd 在同一个栈上抢同一个端口,那是真冲突。实践里,如果你的目标是”SSH 进这台机器干活”,Windows 的 sshd 通常已经够用;而如果你真正想要的是让 SSH 会话落进 Linux,把 DefaultShell 指到 C:\Windows\System32\wsl.exe,比跑第二个守护进程更省事。
能同时监听 22 和自定义端口吗?#
能,而且这正是迁移的正确姿势。sshd_config 接受多行 Port,并在所有端口上监听:
Port 22
Port 2222
先给新端口补防火墙规则,再重启,从客户端把两个端口都验证一遍,之后才轮到下线 22 端口——如果你的计划是这样的话。防火墙切换也是同一个道理:永远不要一步到位地移动一个正在工作的监听器连同它的网络路径。
免密登录 Git 和 VS Code Remote SSH 能用吗?#
都能用,各有一个说明。OpenSSH 客户端这一侧是完全通用的:你这把 id_ed25519 对 GitHub、GitLab 或任何 Git 托管主机的行为和 Linux 上一模一样,ssh -T git@... 这类测试照常工作——Windows 没有改变协议的任何部分。VS Code 的 Remote-SSH 扩展连进这台 Windows 机器也工作得很好,是远程开发 Windows 机器的舒服姿势;先把 DefaultShell 设成 PowerShell,因为这个扩展靠 shell 命令驱动远端,期待的是一个够格的 shell。要说说明的是:让这台 Windows 机器自己通过 SSH 提供 Git 仓库服务是另一个工程——原装 sshd 给你的是 shell 和 SFTP,不含 git-receive-pack 那套管道。把 Windows 机器当 SSH 端点随便用;真要架 Git 服务器(Gitea、Forgejo、GitLab),那就另起炉灶。
结语#
版本分界线就是这张地图:1809 及以后是两条命令的功能安装,更老的系统走 Win32-OpenSSH 的 zip 包或者搭既有 Cygwin 的便车,而安装之后的一切都汇聚到同一个 C:\ProgramData\ssh 目录、同一套规则。把三件事做对,剩下的都是细节——启动服务并设为自动、确认防火墙规则真的覆盖你监听的端口、在配置第一把管理员密钥之前弄懂 administrators_authorized_keys 那个分叉,因为它的失败方式是沉默。等密钥登录验证可用,再加上 PasswordAuthentication no;把 DefaultShell 指向 PowerShell,让会话配得上”值得开”三个字;记住 sshd -d 这个能把看不见的认证失败变成可读文字的工具。一台加固好的 Windows SSH 端点是一件安静、无聊的基础设施——而这恰恰是基础设施能得到的最高评价。