Install-SshKey.ps1
|
$ErrorActionPreference = "Stop" $administratorSid = "S-1-5-32-544" $isAdministrator = whoami /groups /fo csv | ConvertFrom-Csv | Where-Object { $_.SID -eq $administratorSid -and $_.Attributes -match "Enabled group" } function Resolve-AuthorizedKeysPath { param( [string]$ConfiguredPath ) $path = $ConfiguredPath.Trim().Trim('"') -replace '/', '\' if([string]::IsNullOrWhiteSpace($path) -or $path -ieq 'none') { return $null } $path = $path -replace '^__PROGRAMDATA__', $env:ProgramData $path = $path.Replace('%h', $env:USERPROFILE).Replace('%u', $env:USERNAME) if($path.StartsWith('~\')) { return Join-Path $env:USERPROFILE $path.Substring(2) } if([System.IO.Path]::IsPathRooted($path)) { return [Environment]::ExpandEnvironmentVariables($path) } return Join-Path $env:USERPROFILE $path } function Get-ConfiguredAuthorizedKeysFiles { $defaultAuthorizedKeys = @('.ssh/authorized_keys') $configPath = Join-Path $env:ProgramData 'ssh\sshd_config' if(!(Test-Path -LiteralPath $configPath)) { return $defaultAuthorizedKeys } $globalAuthorizedKeys = $null $administratorAuthorizedKeys = $null $inMatchBlock = $false $matchAppliesToAdministrator = $false foreach($rawLine in Get-Content -LiteralPath $configPath) { $line = ($rawLine -replace '\s+#.*$', '').Trim() if([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) { continue } if($line -match '^Match\s+(.+)$') { $inMatchBlock = $true $matchCondition = $Matches[1] $matchAppliesToAdministrator = $isAdministrator -and ( $matchCondition -match '(^|\s)all(\s|$)' -or $matchCondition -match '(^|\s)group\s+("[^"]*administrators[^"]*"|\S*administrators\S*)' ) continue } if($line -match '^AuthorizedKeysFile\s+(.+)$') { $authorizedKeysFiles = $Matches[1] -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if($inMatchBlock) { if($matchAppliesToAdministrator -and -not $administratorAuthorizedKeys) { $administratorAuthorizedKeys = $authorizedKeysFiles } } elseif(-not $globalAuthorizedKeys) { $globalAuthorizedKeys = $authorizedKeysFiles } } } if($administratorAuthorizedKeys) { return $administratorAuthorizedKeys } if($globalAuthorizedKeys) { return $globalAuthorizedKeys } return $defaultAuthorizedKeys } $authorizedKeys = Get-ConfiguredAuthorizedKeysFiles | ForEach-Object { Resolve-AuthorizedKeysPath $_ } | Where-Object { $_ } | Select-Object -First 1 if(-not $authorizedKeys) { throw 'No usable AuthorizedKeysFile path is configured.' } $sshDirectory = Split-Path -Parent $authorizedKeys New-Item -ItemType Directory -Path $sshDirectory -Force | Out-Null $encodedKeyText = "__ENCODED_KEY_TEXT__" $keyText = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encodedKeyText)) $keyLines = $keyText -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($keyLines.Count -eq 0) { throw "No public key content was received." } # --- 可写性检测与权限修复 --- # administrators_authorized_keys 常由 SYSTEM 预先创建并持有(仅 SYSTEM 可写), # 管理员会话直接写入也会 Access denied。处理策略: # 1) takeown 夺取所有权 + icacls 授予 Administrators 完全控制; # 2) 恢复 sshd 要求的严格 ACL(属主 Administrators 组、禁用继承、仅 SYSTEM/Administrators 可访问); # 3) 都失败则回退写入用户级 ~\.ssh\authorized_keys 并给出明确警告。 function Test-FileWritable { param([string]$Path) try { $fs = [System.IO.File]::Open($Path, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write) $fs.Close() return $true } catch { return $false } } function Repair-AuthorizedKeysAcl { param([string]$Path) try { $null = & takeown.exe /f $Path 2>&1 if($LASTEXITCODE -ne 0) { return $false } $null = & icacls.exe $Path /grant ($env:USERNAME + ':(F)') 2>&1 if($LASTEXITCODE -ne 0) { return $false } # 恢复 sshd 要求的严格 ACL(ACL 不合规时 sshd 会拒绝读取该文件): # 属主为 Administrators 组,禁用继承,仅 SYSTEM 与 Administrators 拥有完全控制 $systemSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18") $administratorsSid = New-Object System.Security.Principal.SecurityIdentifier($administratorSid) $acl = New-Object System.Security.AccessControl.FileSecurity $acl.SetOwner($administratorsSid) $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($systemSid, "FullControl", "Allow"))) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($administratorsSid, "FullControl", "Allow"))) Set-Acl -LiteralPath $Path -AclObject $acl return $true } catch { return $false } } if(-not (Test-FileWritable -Path $authorizedKeys)) { Write-Warning "目标文件不可写:$authorizedKeys,尝试修复 ACL……" $repaired = Repair-AuthorizedKeysAcl -Path $authorizedKeys if(-not $repaired -or -not (Test-FileWritable -Path $authorizedKeys)) { $fallbackKeys = Join-Path $env:USERPROFILE '.ssh\authorized_keys' if([string]::Equals($authorizedKeys, $fallbackKeys, [System.StringComparison]::OrdinalIgnoreCase)) { throw "No write permission to $authorizedKeys (ACL repair also failed)." } Write-Warning "修复失败,回退到用户级文件:$fallbackKeys(sshd_config 若为管理员指定了独立密钥文件,该回退可能不生效;请在远程以管理员身份为 Administrators 组授予 ${env:ProgramData}\ssh\administrators_authorized_keys 的写入权限后重试)" $authorizedKeys = $fallbackKeys $sshDirectory = Split-Path -Parent $authorizedKeys New-Item -ItemType Directory -Path $sshDirectory -Force | Out-Null } } $existingKeys = @() if (Test-Path -LiteralPath $authorizedKeys) { $existingKeys = Get-Content -LiteralPath $authorizedKeys -Raw -ErrorAction SilentlyContinue if ($null -ne $existingKeys -and $existingKeys.Length -gt 0 -and -not $existingKeys.EndsWith("`n")) { [System.IO.File]::AppendAllText($authorizedKeys, [Environment]::NewLine, [Text.UTF8Encoding]::new($false)) } $existingKeys = @(Get-Content -LiteralPath $authorizedKeys -ErrorAction SilentlyContinue) } # 提取密钥材质(类型+Base64,不含注释)用于去重比较 $existingKeyMaterials = $existingKeys | ForEach-Object { ($_ -split '\s+')[0..1] -join ' ' } foreach ($keyLine in $keyLines) { $keyMaterial = ($keyLine -split '\s+')[0..1] -join ' ' if ($existingKeyMaterials -notcontains $keyMaterial) { [System.IO.File]::AppendAllText($authorizedKeys, $keyLine + [Environment]::NewLine, [Text.UTF8Encoding]::new($false)) $existingKeys += $keyLine $existingKeyMaterials += $keyMaterial } } if ($isAdministrator -and ([string]::Equals($authorizedKeys, (Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'), [System.StringComparison]::OrdinalIgnoreCase))) { # 仅在需要时恢复 sshd 要求的严格 ACL;失败不视为安装失败(密钥已写入, # 但若 sshd 因 ACL 不合规而拒绝读取,请远程手动执行: # icacls ...administrators_authorized_keys) try { $systemSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18") $administratorsSid = New-Object System.Security.Principal.SecurityIdentifier($administratorSid) $acl = New-Object System.Security.AccessControl.FileSecurity $acl.SetOwner($administratorsSid) $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($systemSid, "FullControl", "Allow"))) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($administratorsSid, "FullControl", "Allow"))) Set-Acl -LiteralPath $authorizedKeys -AclObject $acl } catch { Write-Warning "已写入公钥,但恢复 administrators_authorized_keys 的严格 ACL 失败:$_。若 sshd 拒绝读取该文件,请在远程以管理员身份手动修正其 ACL。" } } |