返回博客

Windows CI 构建机:MSYS-ssh 读 known_hosts 失败的 NULL DACL 陷阱

给一台新的 Windows LAN 构建机做初始化:用脚本把 known_hosts 通过 scp 推上去,然后跑第一次 git fetch,结果:

hostkeys_find_by_key_hostfile: hostkeys_foreach failed for /c/Users/builder/.ssh/known_hosts: Permission denied
Host key verification failed.
fatal: Could not read from remote repository.

文件存在,大小不为零,当前用户就是 owner——为什么会 Permission denied?

现象

错误路径是 /c/Users/...,MSYS 风格,而非 Windows 原生的 C:\Users\...。这个细节是关键线索:报错的是 Git for Windows 内置的 MSYS bundled OpenSSH,不是系统自带的 C:\WINDOWS\System32\OpenSSH\ssh.exe

奇怪的是,用 PowerShell 的 Get-Content 直接读同一文件:

Get-Content "$env:USERPROFILE\.ssh\known_hosts" -TotalCount 1
# UnauthorizedAccessException

也报权限拒绝。再查 ACL:

icacls "$env:USERPROFILE\.ssh\known_hosts"
# C:\Users\builder\.ssh\known_hosts
# Successfully processed 1 files.

注意:icacls 只输出了文件路径行和汇总行,中间没有任何 ACE 条目。再查 SDDL:

(Get-Acl "$env:USERPROFILE\.ssh\known_hosts").Sddl
# O:S-1-5-21-...G:...D:AI

D:AI ——DACL 段只有一个 AISE_DACL_AUTO_INHERITED)标志,没有任何 (A;...) ACE 子句。这就是 NULL DACL。

根因

Windows 的文件访问控制要求 DACL 中存在明确的 FILE_GENERIC_READ ACE,才能读取文件内容。"你是 owner"只赋予 READ_CONTROL + WRITE_DAC(读/改 ACL 本身的权限),不赋予读文件内容的权限。

问题的来源是初始化脚本中的一步:锁定 .ssh 目录时用了非继承 ACE:

icacls .ssh /inheritance:r
icacls .ssh /grant:r "builder:F"   # 没有 (OI)(CI) —— 不会被子文件继承!

/grant:r user:F 缺少 (OI)(CI) 标记,ACE 只作用于目录本身,不传播给子文件。于是之后 scp 进来的 known_hosts 继承到的是空 DACL + AI 标志——owner 存在,读 ACE 不存在。

Windows 原生 ssh.exe 碰巧走了另一条代码路径,能读成功,掩盖了这个问题;MSYS bundled ssh 做了更严格的 POSIX 风格权限翻译,直接 EACCES。

坑:Windows 所有权 ≠ 读权限,NULL DACL 让 owner 也读不了文件

D:AI SDDL + icacls 无 ACE 行 = NULL DACL。文件 owner 只有改 ACL 的权利,没有读文件内容的权利。Windows 原生工具可能恰好绕过了这个检查,导致问题在 MSYS/Git for Windows 环境才暴露。

正解

修复已有文件

$kh = "$env:USERPROFILE\.ssh\known_hosts"
icacls $kh /inheritance:r /grant:r "$($env:USERNAME):F" /grant:r "SYSTEM:F"

三个关键参数:

  • /inheritance:r — 清除 D:AI 标志,从头开始写 DACL
  • /grant:r user:F — 给 owner 显式 Full Control ACE(这是缺失的那一条)
  • /grant:r SYSTEM:F — 惯例,允许备份/日志/sshd 访问

修复根源:让父目录的 ACE 可继承

$ssh = "$env:USERPROFILE\.ssh"
icacls $ssh /inheritance:r `
            /grant:r "$($env:USERNAME):(OI)(CI)F" `
            /grant:r "SYSTEM:(OI)(CI)F"

(OI)(CI) = Object Inherit + Container Inherit,之后 scp 进来的任何文件都会自动继承这两条 ACE,不再出现空 DACL。

验证

# 1. 确认 ACE 存在
icacls "$env:USERPROFILE\.ssh\known_hosts"
# 期望看到两行 ACE:NT AUTHORITY\SYSTEM:(F) 和 <HOST>\builder:(F)
 
# 2. 确认可以读
Get-Content "$env:USERPROFILE\.ssh\known_hosts" -TotalCount 1
# 期望输出第一条 host 记录,不抛异常
正解:icacls 加 (OI)(CI) 让子文件继承读权限

父目录 /grant:r user:(OI)(CI)F 使 ACE 传播给目录内所有新建/scp 进来的文件。已存在的 known_hosts 用 /inheritance:r /grant:r user:F 补一条显式 ACE 即可。

诊断一键命令
$f = "$env:USERPROFILE\.ssh\known_hosts"
Write-Host ("exists=" + (Test-Path $f) + " size=" + (Get-Item $f).Length)
Write-Host ("owner=" + (Get-Acl $f).Owner)
Write-Host ("sddl=" + (Get-Acl $f).Sddl)
icacls $f
try { Get-Content $f -TotalCount 1 | Out-Null; Write-Host "READ OK" }
catch { Write-Host ("READ FAIL: " + $_.Exception.Message) }

NULL DACL 特征:exists=True、size>0、owner 是自己、sddl 里 D:AI(A;...) 子句、icacls 无 ACE 行、READ FAIL。

一句话外卖

Windows 文件权限里,"你是 owner"只意味着你能改 ACL,不意味着你能读文件——scp 进来的文件如果父目录 ACE 没有 (OI)(CI),会悄悄继承到空 DACL,原生工具可能恰好不触发,MSYS 工具会直接 EACCES。