ssh-copy-id 说成功了但还是密码登录:Windows OpenSSH 的三层陷阱
搭一台 Windows LAN 构建机,需要从 macOS 推公钥实现免密 SSH。ssh-copy-id 运行一切正常,显示 Number of key(s) added: 1,但随后 ssh 仍然弹密码,-o BatchMode=yes 下直接 Permission denied (publickey,password,keyboard-interactive)。反复试了半小时 flag,毫无进展。
排查下来是三个坑同时叠加,缺一不可。
现象
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub user@windows-host
user@windows-host's password:
'exec' 不是内部或外部命令 # Windows cmd.exe 报错
Number of key(s) added: 1 # 谎报成功
$ ssh -o BatchMode=yes user@windows-host 'echo ok'
Permission denied (publickey,password,keyboard-interactive).根因
坑 1:ssh-copy-id 在 Windows OpenSSH 上静默失败
ssh-copy-id 的实现方式是登进目标机,注入一段 bash one-liner(umask 077; exec sh -c 'cd; cat >> ~/.ssh/authorized_keys')。Windows OpenSSH Server 默认 shell 是 cmd.exe,不认识 exec,整段脚本部分失败或什么都没写进去。但工具的"成功计数"来自另一个验证步骤,返回了假阳性。
它向目标机注入 bash 脚本,Windows cmd.exe 会让 exec 命令直接报错。Number of key(s) added: 1 是假成功,不要继续换 flag 重试——换工具。
坑 2:Admin 用户的公钥文件路径不是你以为的那个
Windows OpenSSH 自 7.7 起,对本地 Administrators 组成员,sshd 完全忽略 %USERPROFILE%\.ssh\authorized_keys,只读 C:\ProgramData\ssh\administrators_authorized_keys(全局一个文件,所有 admin 共享)。写错路径,sshd 视而不见。
检测用户组时不要用 net localgroup administrators——中文 locale 下组名变成 管理员,命令会报"系统找不到指定的路径"。改用 SID(与 locale 无关):
ssh user@host 'whoami /groups | findstr S-1-5-32-544'S-1-5-32-544 是 BUILTIN\Administrators 的固定 SID,有输出 = 是管理员,写全局文件;无输出 = 普通用户,写 per-user 路径。
坑 3:icacls 括号参数在 Shell 调用栈里被肢解
sshd 对 authorized_keys 的 ACL 有严格要求:必须断继承、只保留文件所有者和 SYSTEM。稍微宽松一点(比如继承来的 Authenticated Users 权限残留),sshd 静默拒绝公钥,Event Log 才有记录,stderr 什么都不说。
设置 ACL 的自然写法会出错:
icacls $authFile /inheritance:r /grant:r "${env:USERNAME}:(F)"
# 错误: 无效参数 "(F)"原因是调用链 bash → ssh → cmd.exe → powershell → icacls.exe,括号 (F) 在某一层被拆成独立参数,icacls 收到时已经不合法。
(F) 是 icacls 文档里的写法,但通过 SSH 跨 cmd + PowerShell 调用时括号会被拆碎。直接用裸字母 F 即可,icacls 同样接受。
正解
从 macOS/Linux 客户端驱动,用 scp 传 PowerShell 脚本再执行,彻底绕开行内 quoting 地狱。
1. 探测用户组
ssh user@host 'whoami /groups | findstr S-1-5-32-544'
# 有输出 → admin,写 C:\ProgramData\ssh\administrators_authorized_keys
# 无输出 → 普通用户,写 %USERPROFILE%\.ssh\authorized_keys2. 生成安装脚本(以普通用户为例)
PUB_KEY=$(cat ~/.ssh/id_ed25519.pub)
TMP_PS1=$(mktemp -t install-key.XXXXXX.ps1)
cat > "$TMP_PS1" <<PSEOF
\$pubKey = '${PUB_KEY}'
\$sshDir = "\$env:USERPROFILE\.ssh"
\$authFile = "\$sshDir\authorized_keys"
\$me = \$env:USERNAME
if (!(Test-Path \$sshDir)) { New-Item -ItemType Directory -Path \$sshDir -Force | Out-Null }
Set-Content -Path \$authFile -Value \$pubKey -Encoding ASCII -NoNewline
& icacls \$authFile /inheritance:r | Out-Null
& icacls \$authFile /grant:r "\${me}:F" | Out-Null
& icacls \$authFile /grant:r "SYSTEM:F" | Out-Null
& icacls \$sshDir /inheritance:r | Out-Null
& icacls \$sshDir /grant:r "\${me}:F" | Out-Null
& icacls \$sshDir /grant:r "SYSTEM:F" | Out-Null
Write-Host "DONE"; & icacls \$authFile
PSEOF3. 推送并执行
SSHPASS="$PW" sshpass -e scp -o StrictHostKeyChecking=accept-new \
"$TMP_PS1" user@host:install-key.ps1
SSHPASS="$PW" sshpass -e ssh -o StrictHostKeyChecking=accept-new user@host \
"powershell -NoProfile -ExecutionPolicy Bypass -File install-key.ps1 ; del install-key.ps1"
rm -f "$TMP_PS1"4. 验证
ssh -o BatchMode=yes -o ConnectTimeout=5 user@host 'hostname'
# 直接打印 hostname,不弹密码 = 成功BatchMode=yes 禁用所有交互提示,公钥失败立即报 Permission denied,是最干净的验证信号。
不要用 ssh-copy-id;用 scp 传一个 .ps1 然后 powershell -File 执行;icacls 权限标记去掉括号写 F 而非 (F);Admin 用户写 C:\ProgramData\ssh\administrators_authorized_keys。
排查 ACL 残留问题
如果验证仍失败,检查 ACL:
ssh user@host 'icacls "%USERPROFILE%\.ssh\authorized_keys"'看到 (I)(继承 ACE)、BUILTIN\Users、Authenticated Users 中任意一项 = sshd 会静默拒绝。sshd 的详细拒绝原因在 Windows Event Log,不在 stderr:
ssh user@host 'powershell -Command "Get-WinEvent -LogName OpenSSH/Operational -MaxEvents 10 | Format-List TimeCreated,Message"'一句话外卖
ssh-copy-id在 Windows OpenSSH 上只是谎报成功的 bash 脚本注入器;真正正确的路是 scp 一个 PowerShell 文件过去执行,Admin 用户写全局文件,icacls权限标记去掉括号。