1. Windows Server 2016 装 OpenSSH 为什么没法一条命令搞定
如果你是从 Windows Server 2019 或者 Windows 10 新版本迁过来的运维,第一次在 Windows Server 2016 上敲Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0,大概率会吃一个闭门羹——系统告诉你找不到这个功能名。这不是命令写错了,而是 2016 这代系统里压根就没有内置的 OpenSSH Server 可选功能。搜索"Windows Server 2016 安装 OpenSSH Server",能翻出来的答案五花八门,有的让你改注册表,有的让你装第三方 SSH 服务端,真正能跑通的其实只有一条路。
1.1 2016 与 2019 的功能边界差在哪
Windows Server 2016 和 Windows 10 1607 是同一代内核衍生出来的,当年微软还没有把 OpenSSH 作为系统组件收编进去。那套基于Add-WindowsCapability和Get-WindowsCapability的功能安装机制,直到 Windows 10 1809 和 Windows Server 2019 才正式上线。所以你在 2016 上执行:
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'结果会是空的,一行都不返回。这是判断系统支不支持内置安装的最直接方法,比记版本号靠谱得多。只要这条命令没输出,就别再纠结"是不是我参数写错了",直接走后面的发布包路线。
1.2 三条常见路线的取舍
实际能落地的方式有三种,各有各的适用场景:
| 方案 | 优点 | 缺点 | 建议场景 |
|---|---|---|---|
| 微软 Win32-OpenSSH 发布包 | 官方维护、功能完整、和 Linux 侧行为一致 | 需要手工解压注册服务、升级要手动替换 | 绝大多数生产场景 |
| 系统内置功能 | 一条命令搞定、随系统更新 | 2016 上根本不存在 | 仅 2019 及以上 |
| 第三方 SSH 服务端 | 带图形化管理界面 | 授权成本、行为差异、审计不透明 | 有特殊合规要求时 |
我的建议很明确:Windows Server 2016 场景一律走 Win32-OpenSSH 发布包。它是微软自己在维护的项目,加密算法、协议实现、配置文件语法都跟 Linux 上的 OpenSSH 保持高度一致,你从 Linux 侧迁过来的脚本和密钥体系可以直接复用,不用重新学一套。
1.3 版本选择这件小事,其实很关键
Win32-OpenSSH 的发布节奏是跟着上游 OpenSSH 走的,从早期的 v7.7 到后来的 v8.x、v9.x,差异不小。2016 是较老的系统,新版发布包对它的支持情况需要留意。
- v7.7.2.0p1-Beta:兼容性最好,2016 上几乎没有问题,但上游这个版本的安全修复早就停了,只适合完全内网、隔离环境。
- v8.9.1.0p1-Beta:我个人在 2016 上用得最多的一版,稳定性好,
ssh-agent和 SFTP 都正常,算是平衡点。 - v9.x 系列:算法更新、安全性更好,但部分版本对 2016 的 .NET 运行时和 PowerShell 版本有额外要求,装之前务必先在测试机上跑一遍。
提示:版本号里带 "p1" 的是基于上游哪个补丁版本,带 "Beta" 是微软自己的打包标记,不代表不能用。生产环境建议先在测试机验证一轮再推。
2. 装之前先盘清楚环境,省得中途翻车
很多人一上来就解压、跑脚本,结果卡在服务注册失败、脚本报错、防火墙没生效上,来回折腾半天。花十分钟把下面这几件事确认完,后面能省掉至少一个小时。
2.1 PowerShell 版本和运行时检查
Win32-OpenSSH 的安装脚本依赖 PowerShell 5.1 和 .NET Framework 4.6 以上。Windows Server 2016 默认就是 PowerShell 5.1 加 .NET 4.6.2,理论上开箱即用,但如果这台机器做过精简或者长期没打补丁,还是确认一下:
$PSVersionTable.PSVersion [System.Environment]::VersionPowerShell 大版本低于 5 的话,install-sshd.ps1里用到的某些 cmdlet 会直接报错。另外,如果这台机器长期没打过系统补丁,建议先把补丁级别更新到一个相对近的时间点,再装 OpenSSH。原因很现实:系统组件更新往往会重置部分服务配置和文件权限,先更新后装,比装完再更新要省心得多。
2.2 权限与网络的准备
安装过程需要写入C:\Program Files\并注册 Windows 服务,所以必须用本地管理员身份操作。注意是本地管理员,不是域里随便一个有权限的账号;如果这台机器加入了域,建议直接用本机 Administrator 或者域管理员,避免 UAC 令牌过滤导致脚本执行到一半失败。
网络这块分两种:
- 能出网:直接在服务器上下载发布包最省事,但要注意企业代理环境可能拦截下载。
- 纯内网:在能上网的机器下载好压缩包,用内网共享或者移动介质拷进去。
2.3 离线拷贝进来的包,记得先解锁
从别的机器拷过来的压缩包,Windows 会给它打上"来自其他计算机"的标记(Zone.Identifier 数据流),解压出来的脚本可能被限制执行。稳妥做法是解压后跑一次:
Get-ChildItem -Path 'C:\Program Files\OpenSSH' -Recurse | Unblock-File这行命令看起来不起眼,但我见过至少三次"脚本明明存在却提示无法加载"的案例,根因都是这个标记。顺手做了,能避免一类很莫名其妙的报错。
3. Win32-OpenSSH 落地安装的完整过程
前面铺垫完了,现在进入正题。整个过程拆成四步:解压、注册服务、配防火墙与自启、验证。
3.1 为什么解压路径必须固定
官方脚本默认假设程序目录是C:\Program Files\OpenSSH。这个路径不是随便定的,因为install-sshd.ps1在注册服务时,会把sshd.exe的完整路径写进服务配置里。如果你先解压到D:\Tools\OpenSSH跑完脚本,之后又觉得不合适挪到C:\Program Files\,服务启动时会直接报"系统找不到指定的文件",因为注册表里记的还是老路径。
真要先放别处也不是不行,但后续升级、改配置都得手工同步服务路径,纯属给自己找麻烦。直接按官方约定放C:\Program Files\OpenSSH,最省心。
解压之后目录里应该能看到这些关键文件:
sshd.exe:服务端主程序ssh.exe、scp.exe、sftp.exe:客户端工具ssh-keygen.exe:密钥生成install-sshd.ps1、uninstall-sshd.ps1:安装卸载脚本sshd_config_default:配置模板
3.2 install-sshd.ps1 到底做了什么
这一步是整个安装的核心,很多人只知道跑脚本,不知道它背后改了什么。用管理员权限打开 PowerShell,切到目录执行:
cd 'C:\Program Files\OpenSSH' powershell.exe -ExecutionPolicy Bypass -File .\install-sshd.ps1脚本干的事情大致是四件:
- 注册两个 Windows 服务:
sshd(SSH 服务端)和ssh-agent(密钥代理)。 - 设置启动类型:
ssh-agent默认设为自动启动,sshd默认设为手动启动——这点很重要,意味着光跑完脚本服务还不会自己起来,得手动启动并改成自动。 - 生成主机密钥:如果
C:\ProgramData\ssh\下不存在ssh_host_*_key这些文件,脚本会调用ssh-keygen生成一套,包括 RSA、ECDSA、ED25519 等。这些是服务器身份标识,客户端首次连接时看到的指纹就来自这里。 - 设置文件和目录权限:把主机密钥的访问权限收紧,避免普通用户读取。
执行成功的输出类似这样:
Installing sshd service... sshd service installed successfully. Installing ssh-agent service... ssh-agent service installed successfully.看到这两行就说明服务注册成功了。如果出现红色报错,八成是权限不够或者 PowerShell 执行策略挡住了脚本,加上-ExecutionPolicy Bypass参数基本能解决。
3.3 服务自启、防火墙、环境变量三件套
脚本跑完还不算完,下面三件事补齐,SSH 才算真正可用。
第一,把 sshd 改成自动启动并拉起来:
Set-Service -Name sshd -StartupType Automatic Start-Service sshd Get-Service sshd, ssh-agentssh-agent保持自动启动,sshd也设成自动,这样机器重启后不用人工干预。
第二,放行 22 端口:
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' ` -DisplayName 'OpenSSH Server (sshd)' ` -Enabled True -Direction Inbound -Protocol TCP ` -Action Allow -LocalPort 22如果你把监听端口改成了别的,这里记得同步。有个细节值得一提:install-sshd.ps1在新一些的版本里会自动尝试创建这条防火墙规则,但老版本和某些精简系统上不一定生效,手工补一条最保险。
第三,把目录加进 PATH:
$oldPath = [Environment]::GetEnvironmentVariable('Path', 'Machine') if ($oldPath -notlike '*OpenSSH*') { [Environment]::SetEnvironmentVariable('Path', "$oldPath;C:\Program Files\OpenSSH", 'Machine') }加 PATH 的目的是让你在任何目录下都能直接用ssh、scp、sftp这些客户端命令,尤其是做自动化脚本的时候,不用写死全路径。
3.4 装完必须跑的验证清单
别急着从别的机器连过来,先在本机自检一遍,出问题定位更快:
# 1. 服务状态是否为 Running Get-Service sshd | Select-Object Name, Status, StartType # 2. 配置是否合法(-T 会输出最终生效的配置) & 'C:\Program Files\OpenSSH\sshd.exe' -T # 3. 22 端口是否在监听 Get-NetTCPConnection -LocalPort 22 -State Listen # 4. 本机自连测试 ssh -o StrictHostKeyChecking=no localhost "whoami"sshd -T这个命令特别实用,它会把配置文件和默认值合并后输出最终生效的每一项参数。改完sshd_config不知道有没有生效,跑一下这个就知道了,比反复重启服务试错高效得多。
4. sshd_config 里真正影响使用体验的几个参数
服务跑起来只是第一步,默认配置下用起来别扭的地方不少。这一节挑几个最影响体验的参数展开说。
4.1 默认 Shell 换成 PowerShell 的两种做法
默认登录进去落到的是cmd.exe。对于习惯 PowerShell 的人,或者跑自动化脚本的,这个体验很差。换默认 Shell 有两种方式:
方式一:改sshd_config里的DefaultShell(需要较新版本支持)
DefaultShell powershell.exe方式二:改注册表(我推荐这种)
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell ` -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' ` -PropertyType String -Force为什么更推荐注册表?因为注册表方式不依赖 OpenSSH 版本,而且在你想从 PowerShell 5.1 切到 PowerShell 7 的时候,只改一个路径就行,不用动sshd_config。另外如果同时装了多个 PowerShell 版本,注册表方式切换起来干净利落。
注意:改完这两处都要重启
sshd服务才生效。重启命令是Restart-Service sshd,不要图省事去Stop-Service之后就忘了Start-Service。
4.2 公钥登录里那个最容易踩的权限坑
管理员组成员用公钥登录时,走的不是C:\Users\用户名\.ssh\authorized_keys,而是C:\ProgramData\ssh\administrators_authorized_keys。这是 Win32-OpenSSH 一个和 Linux 差异很大的设计,也是新手最容易卡住的地方——明明把公钥放进了用户目录,登录时还是提示要密码。
正确做法是把公钥追加到管理员专用文件,然后把权限收紧到只有 SYSTEM 和 Administrators 能读写:
$key = 'ssh-ed25519 AAAAC3Nza...你的公钥内容' Add-Content -Path 'C:\ProgramData\ssh\administrators_authorized_keys' -Value $key icacls 'C:\ProgramData\ssh\administrators_authorized_keys' /inheritance:r ` /grant 'SYSTEM:(F)' /grant 'BUILTIN\Administrators:(F)'这两行是配套的,只做第一行不做第二行,sshd会因为权限过于宽松而拒绝使用这个文件,日志里通常看不到太明显的提示,排查起来很折磨。
如果是普通用户(非管理员组),公钥就放C:\Users\用户名\.ssh\authorized_keys,同样要注意权限,这个目录的继承权限要从上层剥掉一部分:
icacls 'C:\Users\用户名\.ssh\authorized_keys' /inheritance:r ` /grant '用户名:(F)' /grant 'SYSTEM:(F)'4.3 SFTP 子系统和日志级别
需要传文件的场景,确认sshd_config里有这一行:
Subsystem sftp sftp-server.exe注意 Windows 上是sftp-server.exe,不是 Linux 上的/usr/lib/openssh/sftp-server,路径写错了 SFTP 会静默失败,连接能建立但一传文件就断。
排查阶段建议把日志级别调高:
SyslogFacility LOCAL0 LogLevel DEBUG1DEBUG1会把认证过程、密钥加载、权限检查的细节都打出来,定位问题效率极高。等排查完再调回INFO,避免日志文件爆掉。日志默认输出到 Windows 事件查看器的"应用程序和服务日志 → OpenSSH"下面,也可以配SyslogFacility指向本地文件(需要额外组件支持,一般用事件查看器就够)。
4.4 端口、监听范围和并发控制
几个建议按需调整的参数:
| 参数 | 默认值 | 调整建议 |
|---|---|---|
Port | 22 | 对外开放环境改成 20000 以上的高位端口,能挡掉大量扫描 |
ListenAddress | 0.0.0.0 | 只需内网访问可改为指定内网 IP,减少暴露面 |
MaxStartups | 10:30:100 | 高并发场景调到 30:50:200 |
MaxSessions | 10 | 单个连接允许的会话数,做跳板机时适当调大 |
LoginGraceTime | 120 | 建议改小到 30,缩短未认证连接的占位时间 |
ListenAddress这一项在多网卡机器上很实用,比如这台服务器既有公网网卡又有内网网卡,只想让内网访问 SSH,绑内网地址就行,不用额外配防火墙规则。
5. 踩坑复盘:安装过程中最常见卡住的几个点
讲完"应该怎么做",再说说"实际会出什么问题"。这部分是从真实环境里攒下来的,不是从文档抄的。
5.1 服务启动后立刻停止,22 端口起不来
这个现象通常有两个根因。一是配置文件语法错误,比如参数名拼错、缩进里混进了 Tab、行尾有 BOM 头。判断方法很简单:直接前台运行sshd.exe -d,它会把配置解析错误原样打出来,一眼就能看到是第几行的问题。
二是端口被占用。2016 上如果装过其他远程管理组件,22 端口可能已经被占了。查一下:
Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, OwningProcess Get-Process -Id (Get-NetTCPConnection -LocalPort 22).OwningProcess确认占用后要么干掉那个进程,要么把 SSH 换到别的端口。我个人的习惯是,凡是面向较多来源开放的机器,直接把端口设成高位端口,既避开冲突,也减少日志里那些扫描记录。
5.2 中文乱码和换行符问题
authorized_keys、sshd_config这些文件,必须是 UTF-8 无 BOM 编码,换行用 LF。用记事本编辑过之后很容易变成带 BOM 的 UTF-8 或者 CRLF 换行,结果就是配置看着完全正确但服务就是起不来,或者密钥明明贴进去了认证还是失败。
稳妥的做法是用 PowerShell 写入,或者用支持指定编码的编辑器:
$content = Get-Content 'C:\ProgramData\ssh\sshd_config' -Raw [System.IO.File]::WriteAllText('C:\ProgramData\ssh\sshd_config', $content, (New-Object System.Text.UTF8Encoding $false))那个$false就是"不带 BOM"的意思。这行代码在处理配置文件异常时我用了很多次,很值得记下来。
5.3 密钥认证一直退回密码认证
除了前面说的管理员专用文件位置和权限问题,还有两个隐蔽原因。一是sshd_config里PubkeyAuthentication被设成了no,或者AuthorizedKeysFile被改到了别处;二是客户端侧的私钥文件权限过于宽松,Windows 上默认不检查这个,但如果私钥是从 Linux 拷过来的、权限是 777,某些客户端会直接忽略它。
排查顺序建议是:先在服务端把LogLevel调到DEBUG1重启,然后从客户端连一次,去事件查看器里看认证阶段具体走到了哪一步、哪一步被拒了。这个链路走一遍,基本没有定位不了的问题。
5.4 升级和卸载时的残留问题
Win32-OpenSSH 升级不能直接覆盖解压,正确姿势是:
Stop-Service sshd; Stop-Service ssh-agent- 备份
C:\ProgramData\ssh\整个目录(这里面是主机密钥和授权文件,丢了客户端会报指纹变更) - 用新版本的文件替换
C:\Program Files\OpenSSH下的二进制和脚本 Start-Service sshd,然后跑sshd -T确认配置仍然合法
卸载的话执行uninstall-sshd.ps1,但注意脚本只删服务注册,不会删C:\ProgramData\ssh\下的密钥和配置。要彻底清理得手工删那个目录,删之前想清楚,因为主机密钥一旦没了,所有客户端的known_hosts都要重新确认。
5.5 系统打补丁或重启之后,一定要回头验证
这一点是被不少同行忽略的。Windows Server 2016 上装完系统更新、重启之后,个别服务组件的状态和配置可能会发生变化——比如某些会话类服务在特定补丁重启后出现会话异常、需要重新确认配置,这类情况在社区里时不时有人反馈。SSH 虽然不至于直接断,但sshd的自启状态、监听端口、防火墙规则这三项,重启后花两分钟复查一遍是很值得的:
Get-Service sshd, ssh-agent | Select-Object Name, Status, StartType Get-NetTCPConnection -LocalPort 22 -State Listen Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' | Select-Object Enabled把这三行写进一个巡检脚本,丢进计划任务里定期跑,出问题能第一时间发现。我吃过一次亏——某次系统更新后sshd的启动类型被改回了手动,重启后 SSH 直接不通,人还在外地,只能走带外管理进去处理,相当被动。
6. 安全加固:把这台机器的 SSH 暴露面收窄
装好只是及格线,真正要在生产环境长期跑,加固这一步不能省。SSH 服务一旦对外开放,扫描和暴力破解是必然的,区别只在于你能不能挡住。
6.1 关掉密码登录,只留公钥
这是性价比最高的一条加固措施。暴力破解本质上是在猜密码,把密码认证关掉,这类攻击直接失效:
PasswordAuthentication no PubkeyAuthentication yes PermitEmptyPasswords no顺序很重要:一定要先确认公钥登录已经能正常用,再关密码登录。反过来的话,自己就被锁在门外了,只能去带外管理或者控制台救场。我一般会保留一个测试用的 SSH 会话不关,改完配置重启服务后,用新会话验证一遍,确认没问题再断开老会话。
6.2 限制可登录的账号和来源
AllowGroups ssh-users DenyUsers administratorAllowGroups配合一个专门的本地组使用,效果比逐个列用户好维护。做法是建一个本地组,把需要 SSH 的账号加进去,sshd_config里只放组名。人员变动时改组成员就行,不用动配置文件。
如果管理来源是固定的几个网段,还可以在防火墙层面做来源限制:
Set-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' ` -RemoteAddress 10.0.0.0/8, 192.168.1.0/24这一条比任何服务端配置都有效,因为连接在到达sshd之前就被挡掉了。只开放给运维网段,是最简单也最有效的收敛方式。
6.3 登录失败的排查线索在哪
安全加固之后,登录失败是必然会发生的事,关键是有没有留下可追溯的记录。三个地方要看:
- Windows 事件查看器 → 应用程序和服务日志 → OpenSSH → Operational:
sshd自己的日志,认证成功失败、密钥加载过程都在这。 - 安全日志:事件 ID 4624(登录成功)、4625(登录失败),配合登录类型 10(RemoteInteractive)过滤。
sshd前台调试输出:sshd.exe -d跑一次,实时看认证链路。
把这几条线索串起来,能还原出完整的攻击画像——大概什么时候、从哪个 IP、尝试了哪些用户名。看到大量来自同一来源的失败记录,直接防火墙封 IP 就行。
6.4 版本跟进这件长期活儿
OpenSSH 上游的安全公告不算少,尤其是加密算法相关的调整。我的做法是:每季度检查一次当前使用的 Win32-OpenSSH 版本,和上游最新发布对比一下,有安全相关更新就在测试机验证后推。测试机验证的要点是三件事——公钥登录是否正常、SFTP 是否正常、自动化脚本调用的scp/ssh命令是否正常。这三样跑通,生产环境基本就没问题。
另外提醒一句,升级前一定备份C:\ProgramData\ssh\,尤其是主机密钥。丢了这个,所有客户端都会提示服务器指纹变更,对内网大批量机器来说,重新分发known_hosts是个不小的麻烦。
最后分享个小技巧:如果你管理的 2016 服务器不止一台,可以把整个安装过程写成一个带参数的 PowerShell 脚本——传版本号进去自动下载、解压、注册服务、配防火墙、改默认 Shell、设公钥路径。我第一次是手工装了三台,第三台装完就决定写脚本了,后面再来新机器,一条命令五分钟搞定,省下来的时间全用来处理真正棘手的问题。脚本里最值得保留的一段是服务状态和监听端口的自检逻辑,每次装完自动跑一遍,比人肉核对可靠得多。