跟 Windows 打交道久了你会发现,OpenSSH Server 本身不难装,难的是装完之后那套“密钥认证连接设置”总能整出点玄学问题。我有一次给一台 Windows Server 2019 配 sshd,服务正常起来了,密码能连,密钥死活连不上,客户端只回一句 Permission denied (publickey),服务端日志里也看不出明确原因。后来折腾半天才发现,问题不在 sshd_config,而是 authorized_keys 文件被记事本默认存成了 UTF-16 编码。从那天起我就知道,Windows 上配密钥认证,光懂 Linux 那套完全不够。
这篇文章就把 Windows OpenSSH Server 从安装到密钥认证跑通的完整链路捋一遍。准备照做的运维、搞自动化的开发、以及已经被 authorized_keys 整到怀疑人生的朋友,都可以直接对着抄。
1. Windows 上做密钥认证,坑点全在“错位”
1.1 明明是同一个 sshd,换个底座就翻脸
OpenSSH 最早是为 Unix-like 系统设计的,Windows 上跑的同名软件只是“行为接近”,底层账户体系、文件权限、路径规则全都不一样。
Linux 下配密钥认证,核心步骤就三条:把公钥写进 ~/.ssh/authorized_keys,chmod 600 收紧权限,改 sshd_config 开 PubkeyAuthentication。整个过程半小时内能搞定。Windows 上同样的逻辑会遇到更多变量:
- 公钥文件放在哪个目录,普通用户和管理员不一样;
- 文件编码必须是 UTF-8 无 BOM,记事本默认存 UTF-16 会直接让公钥失效;
- ACL 权限比 Unix 的
chmod复杂,一旦带了继承来的 Everyone 权限,sshd 会认为文件不安全; - 配置文件路径在
C:\ProgramData\ssh\sshd_config,不是 OpenSSH 安装目录下; - 服务是 Windows 服务,改完配置不会自动 reload,必须重启
sshd。
这些“错位”累积起来,就会出现“好像每一步都按教程做了,但就是连不上”的情况。
1.2 密钥认证解决的是哪类问题
密钥认证不只是为了少输一次密码。它的核心价值在三个方面:
一是安全性。私钥不出客户端机器,公钥即使泄露也不能反向登录,相比密码认证能防暴力破解和撞库。
二是可自动化。脚本、计划任务、CI/CD 流水线、监控系统要连服务器时,没办法交互输入密码,密钥认证是唯一顺手的方案。Windows 自动化里经常要跨节点执行 PowerShell 脚本,用 SSH 通道比开 PSRemoting 再配信任关系轻量得多。
三是跨平台一致性。平时管理一堆 Linux 和 Windows 混合环境时,同一套密钥、同样的 ssh 命令可以直接复用。比如 Windows 和 Linux 之间传文件,用 scp 或 sftp 走密钥认证,比 SMB 共享和 FTP 更省心。
1.3 先确认你不需要第三方安装包
Windows 10 1809 和 Windows Server 2019 开始,OpenSSH 已经是系统可选功能。没必要跑到“openssh 官网下载”那里折腾安装包,系统自带版本完全够用。只有遇到远程漏洞扫描要求升级版本、或需要新特性时,才考虑用新版覆盖。
判断系统里有没有:
powershell复制Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'
输出里能看到 OpenSSH.Client 和 OpenSSH.Server 两项。今天讲的是 Server 端,Client 端一般默认就在。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把服务端从零跑起来:安装、自启、防火墙
2.1 用 PowerShell 干净安装
以管理员身份打开 PowerShell,执行:
powershell复制Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
安装过程不到一分钟。装完系统里会多出两个服务:sshd 和 ssh-agent。前者是 SSH 服务端,后者是密钥代理服务,后面再讲。
如果你用的是旧方法,从 GitHub 下载 OpenSSH 压缩包解压到 C:\Program Files\OpenSSH,原理一样,但要注意服务可执行文件路径可能指向不同的目录,后面改配置时容易踩坑。
2.2 启动服务并设置开机自启
安装完默认不会自动启动,需要手动处理:
powershell复制Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
Get-Service sshd
看到 Status: Running 就说明服务起来了。如果启动失败,优先怀疑端口被占或配置文件语法有问题。Windows 上 OpenSSH 配置文件的错误提示比较隐晦,可以先看事件日志:
powershell复制Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 20 | Format-List
2.3 防火墙规则别漏
装完服务第一件事就是确认防火墙放行 22 端口:
powershell复制New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
如果之前手动建过同名规则,会报重复,先删掉旧的再建。验证端口监听:
powershell复制Get-NetTCPConnection -LocalPort 22 | Select-Object State, LocalAddress, LocalPort, OwningProcess
Test-NetConnection localhost -Port 22
Test-NetConnection 返回 TcpTestSucceeded : True 就正常了。
2.4 关于“升级 OpenSSH”那点事
系统内置的 OpenSSH 版本更新频率不高,但功能稳定。真要升级,用 winget 或官方 release 包覆盖安装是可行的,装完务必确认 sshd 服务的可执行文件路径确实指向新版本:
powershell复制Get-CimInstance Win32_Service -Filter "Name='sshd'" | Select-Object PathName
如果路径还指向旧的 System32\OpenSSH\sshd.exe,说明服务没被新版本接管,配置也改不到新实例上。这种情况我建议直接卸载重装,或者用官方安装包走标准安装流程,别手动覆盖。
3. 生成客户端密钥并把公钥放到服务器
3.1 客户端密钥的生成参数
在客户端机器上打开 PowerShell,生成密钥对:
powershell复制ssh-keygen -t ed25519 -a 100 -f "$env:USERPROFILE\.ssh\id_ed25519" -C "your_email_or_备注"
如果你要连接的是老版本 OpenSSH,或者对方设备不支持 Ed25519,退而求其次用 RSA 3072:
powershell复制ssh-keygen -t rsa -b 3072 -f "$env:USERPROFILE\.ssh\id_rsa" -C "备注"
生成时会提示设 passphrase。我的建议是:个人电脑上设置 passphrase,配合后面的 ssh-agent 记忆,安全性会高一大截。CI/CD 或无人值守脚本场景才考虑空 passphrase,否则私钥泄露等于大门敞开。
3.2 没有 ssh-copy-id,就用这几招
Linux 上可以用 ssh-copy-id 一键把公钥推送到服务器,Windows 上没这个命令,手动方式有几种。
最简单的是先密码登录服务器,然后把本地公钥内容追加到目标用户的 authorized_keys。比如你本地公钥内容在 id_ed25519.pub 里,服务器目标路径假定是 C:\Users\admin\.ssh\authorized_keys,可以远端执行:
powershell复制mkdir C:\Users\admin\.ssh
Add-Content -Path C:\Users\admin\.ssh\authorized_keys -Value "粘贴你的公钥内容"
如果在 Windows 客户端上通过 PowerShell 远程把公钥推过去,可以用 scp:
powershell复制scp "$env:USERPROFILE\.ssh\id_ed25519.pub" admin@server:C:/Users/admin/.ssh/authorized_keys
注意:scp 这个写法会把远端文件直接覆盖,如果 authorized_keys 已经有其他公钥,千万别这么干。要追加就先把公钥传到临时文件,再远程执行:
powershell复制scp "$env:USERPROFILE\.ssh\id_ed25519.pub" admin@server:C:/Users/admin/.ssh/temp.pub
ssh admin@server "Get-Content C:/Users/admin/.ssh/temp.pub | Add-Content C:/Users/admin/.ssh/authorized_keys; Remove-Item C:/Users/admin/.ssh/temp.pub"
3.3 私钥在客户端也别忘了收紧权限
很多人只盯着服务器的 ACL,忽略了客户端的私钥权限。Windows 的 OpenSSH 客户端在私钥权限太宽松时,会直接报 UNPROTECTED PRIVATE KEY FILE,拒绝加载。
权限设到“只有当前用户和 SYSTEM 能访问”即可:
powershell复制icacls "$env:USERPROFILE\.ssh\id_ed25519" /inheritance:r /grant "$env:USERNAME:(F)" /grant "SYSTEM:(F)"
/inheritance:r 意思是先移除所有继承权限,再显式赋权,避免父目录里的 Users 组读取权限污染私钥文件。
4. authorized_keys:路径、编码、ACL 三道关
4.1 普通用户和管理员读取的不是同一个文件
这是 Windows OpenSSH 最反直觉的地方。默认配置下:
- 非管理员用户读的是
C:\Users\用户名\.ssh\authorized_keys - 管理员组用户读的是
C:\ProgramData\ssh\administrators_authorized_keys
原因在 sshd_config 末尾默认带了一段:
code复制Match Group administrators
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
意思就是你一旦用管理员账号登录,就不再走普通用户路径,而是去读 ProgramData 下的专用文件。这是 Windows OpenSSH 的安全设计:管理员可以改写任何用户目录,如果把管理员公钥放在用户目录里,权限体系会形同虚设。
所以,如果你用管理员账号连接,却把公钥放在 C:\Users\Administrator\.ssh\authorized_keys,客户端永远只会得到 Permission denied。这个坑我见过太多人踩,包括我自己。
4.2 公钥文件格式和粘贴别翻车
OpenSSH 公钥是单行文本,形如:
code复制ssh-ed25519 AAAA... comment
常见问题有两个。
一个是粘贴时弄出换行或多余空格。公钥中间不能有回车,尾部可以加换行,但别把多行内容混进去。有些编辑器会自动换行,粘贴后看起来是一行,实际文件里被插入换行符,直接导致认证失败。
另一个是 Windows 下记事本和 PowerShell 的默认编码坑。authorized_keys 必须是 UTF-8 无 BOM。用记事本另存为时如果默认存成 ANSI 或带 BOM 的 UTF-8,sshd 解析时可能把 BOM 当作公钥内容的一部分,于是认证失败。
标准做法是用 PowerShell 生成或重写文件:
powershell复制$pub = Get-Content C:\Users\admin\.ssh\id_ed25519.pub
$authKeys = "C:\Users\admin\.ssh\authorized_keys"
[System.IO.File]::WriteAllLines($authKeys, $pub, (New-Object System.Text.UTF8Encoding $false))
UTF8Encoding($false) 表示无 BOM。PowerShell 5.1 里 Set-Content -Encoding UTF8 会带 BOM,所以没必要用。
4.3 权限怎么设才算“恰好够用”
Windows OpenSSH 默认开启严格模式(StrictModes),authorized_keys 文件 ACL 如果太宽松,sshd 会直接拒绝使用。常见的错误是文件带着继承权限,里面能看到 Authenticated Users、Users 这类大组。
非管理员用户的 authorized_keys:
powershell复制icacls C:\Users\admin\.ssh\authorized_keys /inheritance:r /grant "admin:(F)" /grant "SYSTEM:(F)"
如果你担心某个服务账户也要读,可以追加管理员组,但越少越好。
管理员组的 administrators_authorized_keys:
powershell复制icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:(F)" /grant "SYSTEM:(F)"
微软文档要求只保留 Administrators 和 SYSTEM,其他一律不要。/inheritance:r 是去掉所有继承来的权限,这一步很重要。
检查权限语法:
powershell复制icacls C:\Users\admin\.ssh\authorized_keys
会列出文件所有 ACE。如果看到除了目标用户和 SYSTEM 之外的账号,就要重新执行上面的命令。
5. sshd_config 里的开关,哪些是真正要动的
5.1 配置文件位置别搞混
Windows OpenSSH 的配置文件路径是:
code复制C:\ProgramData\ssh\sshd_config
不是 C:\Windows\System32\OpenSSH\sshd_config。如果你从 Linux 上拷贝一份配置过来,记得检查里面的绝对路径和权限模块,两者语法相似但细节不同。
改配置前先备份:
powershell复制Copy-Item C:\ProgramData\ssh\sshd_config C:\ProgramData\ssh\sshd_config.bak
5.2 我建议的最小安全配置
带 # 的默认配置项不需要全解开,只改几个关键点:
code复制PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
KbdInteractiveAuthentication no
LogLevel VERBOSE
这里 PasswordAuthentication no 是在确认密钥认证跑通之后才改的。在没准备好之前,先把它留成 yes,否则密钥一有毛病你就被锁在门外。
KbdInteractiveAuthentication no 是为了避免键盘交互式认证绕过密码选项,在某些客户端配置下会引发二次认证。Windows 有些旧版服务端配置里这个概念叫 ChallengeResponseAuthentication,如果配置文件里存在,一并关掉。
LogLevel VERBOSE 是排障利器,生产环境可以跑一段时间后改回 INFO。
5.3 Match 段对管理员用户的影响
默认配置末尾那段 Match Group administrators 一定要保留。它保证管理员走专用公钥文件。如果你手动把它注释了,管理员用户会退回普通用户路径,这样反而更不安全。
如果你希望禁止某个管理员账号用密钥登录,可以在 Match 段里加 DenyUsers:
code复制Match Group administrators
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
DenyUsers administrator
5.4 改完配置一定要记得做两件事
第一件事是检查语法:
powershell复制sshd -t
Windows 上直接用 sshd -t 能提示配置错误,如果没有任何输出,说明配置没问题。
第二件事是重启服务:
powershell复制Restart-Service sshd
Windows OpenSSH 不会自动 reload 配置,很多“改了密码认证没生效”的问题,根源就是忘了重启服务。
6. 一次“什么都对却连不上”的完整排查链路
6.1 客户端第一手证据:ssh -vvv
密钥认证失败时,先别急着改服务器配置,在客户端执行:
powershell复制ssh -vvv admin@server
-vvv 会输出详细日志。重点看这几个关键点:
Offering public key: ...说明客户端正在提交密钥;Server accepts key: ...说明服务端认可这把公钥;Authentications that can continue: publickey,password说明服务器允许的认证方式列表;Permission denied (publickey)说明认证最终失败。
如果日志里出现 no mutual signature algorithm,大概率是服务器 OpenSSH 版本太老,不支持客户端的 Ed25519 或新 SHA-2 签名算法,换成 RSA 试试。
如果客户端本地私钥加载失败,会有类似 Load key "id_ed25519": incorrect passphrase 或 UNPROTECTED PRIVATE KEY FILE 的提示,先解决客户端再查服务端。
6.2 服务端第二手证据:Windows 事件日志
服务端侧,OpenSSH 的事件日志非常关键:
powershell复制Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 30 | Format-List TimeCreated, Message
常见信息有:
Failed publickey for admin from 192.168.1.10 port 51234 ssh2: ED25519 ...说明公钥验证失败,接下来要去查公钥路径和 ACL;User admin from 192.168.1.10 not allowed because listed in DenyUsers说明被显式拒绝;Authentication refused: bad ownership or modes for file ...说明 ACL 权限有问题;GetFileAttributes() failed: ...说明服务端读取公钥文件路径失败,通常是文件不存在或权限不足。
6.3 按优先级排查的顺序
我把这类问题固化成了一个固定排查顺序,照着走能省一半时间:
- 确认 sshd 服务状态是 Running;
- 用
ssh -vvv看客户端输出,排除私钥、算法、网络问题; - 去服务端事件日志看公钥验证路径;
- 确认公钥文件路径正确:普通用户在用户目录,管理员在 ProgramData;
- 用
icacls检查文件 ACL,问题高发区; - 用 PowerShell 重建文件并确认 UTF-8 无 BOM;
- 检查
sshd_config里有没有Match块把账号排除; - 重启服务再试。
如果做完第八步还连不上,再看防火墙和第三方安全软件。八成问题都在前三步,先把那三步做扎实。
7. 把这些年踩过的坑整理成一张清单
7.1 最容易被忽略的那几个坑
这里说几个我在现场排障时高频遇到的状况。
第一个是“改了 sshd_config 没重启”。你在文件里把 PasswordAuthentication 改成 no,但服务没重启,结果客户端还能密码登录。所以排查任何认证问题,先确认服务状态和配置加载时间。
第二个是“管理员账号走了另一条公钥路径”。我见过有人把公钥放在 C:\Users\Administrator\.ssh\authorized_keys,权限设得干干净净,事件日志里还是报错,原因就是 Match 段把管理员引导到了 administrators_authorized_keys。这个文件和普通用户的文件必须分开维护。
第三个是“杀毒软件或安全策略干扰”。企业环境里部分 EDR 会把 authorized_keys 当作可疑文件扫描或隔离,或者在 sshd 启动时拦截它对公钥文件的读取。处理方式不是关杀毒,而是把 C:\ProgramData\ssh 目录加入安全软件的信任名单,再重新测试。
第四个是“公钥文件里混入了空行或隐藏字符”。如果手动编辑时无意中多了一个空行或不可见字符,sshd 可能跳过或报 invalid key。用下面的命令可以检查文件开头三个字节是不是 EF BB BF(BOM):
powershell复制Format-Hex -Path C:\Users\admin\.ssh\authorized_keys | Select-Object -First 1
看到 EF BB BF 开头,就是带 BOM 了,用前面的 PowerShell 重写方法处理。
7.2 可以直接照抄的完整配置流程
以 Windows Server 2022 或 Windows 11 为例,新用户从零到跑通密钥认证,完整逻辑如下:
- 管理员 PowerShell 执行
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0; Start-Service sshd和Set-Service sshd -StartupType Automatic;New-NetFirewallRule ... -LocalPort 22放行端口;- 客户端生成 ed25519 密钥并追加公钥到服务器;
- 服务端确认路径:普通用户
C:\Users\用户名\.ssh\authorized_keys,管理员C:\ProgramData\ssh\administrators_authorized_keys; - 用
icacls收紧 ACL,只留当前用户和 SYSTEM; - 用 UTF-8 无 BOM 保存公钥文件;
- 改写
sshd_config:PubkeyAuthentication yes、PasswordAuthentication no、LogLevel VERBOSE; - 执行
sshd -t检查配置,Restart-Service sshd; - 客户端
ssh -vvv验证,确认日志中Server accepts key; - 确认成功后把
PasswordAuthentication保持为no,并定期检查事件日志。
这套流程跑完,后续新增用户只需要重复第 4 到第 7 步。
7.3 再送一个省事小技巧
Windows 自带的 ssh-agent 值得用起来。很多人给私钥加了 passphrase,每次连接都要输入,体验很差。可以把私钥暂时加载到 agent:
powershell复制Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add "$env:USERPROFILE\.ssh\id_ed25519"
之后同一会话里连接就不再反复输 passphrase。这个服务在 Windows 里默认是禁用的,所以要先启动。
长期使用后我最大的体会是:Windows 上的密钥认证其实不复杂,但它要求你把“路径”、“编码”、“ACL”这三个细节都盯紧。任何一个环节带着 Linux 习惯去处理,都会掉进同一个坑里。现在我自己配完一台新 Windows 服务器,第一件事永远是跑一遍 ssh -vvv 和事件日志,这套流程做熟之后,密钥认证基本就是一次成。
