Uninstall-SshKey.ps1
|
$ErrorActionPreference = "Stop" # 管理员 SID,用于判断当前用户是否以管理员身份运行 $管理员Sid = "S-1-5-32-544" $是否为管理员 = whoami /groups /fo csv | ConvertFrom-Csv | Where-Object { $_.SID -eq $管理员Sid -and $_.Attributes -match "Enabled group" } # 解析 sshd_config 中配置的 AuthorizedKeysFile 路径为实际文件系统路径 function 解析授权密钥路径 { param( [string]$配置路径 ) $路径 = $配置路径.Trim().Trim('"') -replace '/', '\' if([string]::IsNullOrWhiteSpace($路径) -or $路径 -ieq 'none') { return $null } $路径 = $路径 -replace '^__PROGRAMDATA__', $env:ProgramData $路径 = $路径.Replace('%h', $env:USERPROFILE).Replace('%u', $env:USERNAME) if($路径.StartsWith('~\')) { return Join-Path $env:USERPROFILE $路径.Substring(2) } if([System.IO.Path]::IsPathRooted($路径)) { return [Environment]::ExpandEnvironmentVariables($路径) } return Join-Path $env:USERPROFILE $路径 } # 从 sshd_config 中读取 AuthorizedKeysFile 配置(支持 Match 块) function 获取配置的授权密钥文件 { $默认授权密钥 = @('.ssh/authorized_keys') $配置路径 = Join-Path $env:ProgramData 'ssh\sshd_config' if(!(Test-Path -LiteralPath $配置路径)) { return $默认授权密钥 } $全局授权密钥文件 = $null $管理员授权密钥文件 = $null $在匹配块中 = $false $匹配适用管理员 = $false foreach($原始行 in Get-Content -LiteralPath $配置路径) { $行 = ($原始行 -replace '\s+#.*$', '').Trim() if([string]::IsNullOrWhiteSpace($行) -or $行.StartsWith('#')) { continue } if($行 -match '^Match\s+(.+)$') { $在匹配块中 = $true $匹配条件 = $Matches[1] $匹配适用管理员 = $是否为管理员 -and ( $匹配条件 -match '(^|\s)all(\s|$)' -or $匹配条件 -match '(^|\s)group\s+("[^"]*administrators[^"]*"|\S*administrators\S*)' ) continue } if($行 -match '^AuthorizedKeysFile\s+(.+)$') { $授权密钥文件列表 = $Matches[1] -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if($在匹配块中) { if($匹配适用管理员 -and -not $管理员授权密钥文件) { $管理员授权密钥文件 = $授权密钥文件列表 } } elseif(-not $全局授权密钥文件) { $全局授权密钥文件 = $授权密钥文件列表 } } } if($管理员授权密钥文件) { return $管理员授权密钥文件 } if($全局授权密钥文件) { return $全局授权密钥文件 } return $默认授权密钥 } $授权密钥文件路径 = 获取配置的授权密钥文件 | ForEach-Object { 解析授权密钥路径 $_ } | Where-Object { $_ } | Select-Object -First 1 if(-not $授权密钥文件路径) { throw '没有可用的 AuthorizedKeysFile 路径配置。' } # 解码传入的 Base64 编码密钥文本,提取要移除的密钥行 $编码密钥文本 = "__ENCODED_KEY_TEXT__" $密钥文本 = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($编码密钥文本)) $要移除的密钥行 = @($密钥文本 -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($要移除的密钥行.Count -eq 0) { throw "没有收到任何公钥内容。" } if (-not (Test-Path -LiteralPath $授权密钥文件路径)) { # 文件不存在,没有需要移除的密钥,直接退出 exit 0 } # --- 可写性检测与权限修复 --- # 与 Install-SshKey.ps1 一致:administrators_authorized_keys 可能由 SYSTEM 持有, # 管理员会话写入也会 Access denied。先尝试 takeown + icacls 修复;恢复 sshd # 要求的严格 ACL;失败则提示用户手动处理,并直接退出(移除失败不能静默通过)。 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 } $系统Sid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18") $管理员组Sid = New-Object System.Security.Principal.SecurityIdentifier($管理员Sid) $访问控制列表 = New-Object System.Security.AccessControl.FileSecurity $访问控制列表.SetOwner($管理员组Sid) $访问控制列表.SetAccessRuleProtection($true, $false) $访问控制列表.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($系统Sid, "FullControl", "Allow"))) $访问控制列表.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($管理员组Sid, "FullControl", "Allow"))) Set-Acl -LiteralPath $Path -AclObject $访问控制列表 return $true } catch { return $false } } if (-not (Test-FileWritable -Path $授权密钥文件路径)) { Write-Warning "目标文件不可写:$授权密钥文件路径,尝试修复 ACL……" $已修复 = Repair-AuthorizedKeysAcl -Path $授权密钥文件路径 if (-not $已修复 -or -not (Test-FileWritable -Path $授权密钥文件路径)) { Write-Warning "修复 ACL 失败,无法写入目标文件。请在远程以管理员身份先运行:icacls `"$授权密钥文件路径`" /grant BUILTIN\Administrators:F 然后重试 Remove-SshId。" exit 1 } } # 提取要移除密钥的材质(类型+Base64,不含注释) $要移除的密钥材质 = $要移除的密钥行 | ForEach-Object { ($_ -split '\s+')[0..1] -join ' ' } # 读取现有密钥,只保留材质不匹配的行,写回文件 $已有密钥 = @(Get-Content -LiteralPath $授权密钥文件路径 -ErrorAction SilentlyContinue | Where-Object { $行 = $_; $已有材质 = ($行 -split '\s+')[0..1] -join ' '; -not ($要移除的密钥材质 -contains $已有材质) }) # 以 UTF-8(无 BOM)编码写回文件 [System.IO.File]::WriteAllLines($授权密钥文件路径, $已有密钥, [Text.UTF8Encoding]::new($false)) # 如果写入的是 administrators_authorized_keys,修正文件 ACL;失败不视为移除失败(内容已更新) if ($是否为管理员 -and ([string]::Equals($授权密钥文件路径, (Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'), [System.StringComparison]::OrdinalIgnoreCase))) { try { $系统Sid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18") $管理员组Sid = New-Object System.Security.Principal.SecurityIdentifier($管理员Sid) $访问控制列表 = New-Object System.Security.AccessControl.FileSecurity $访问控制列表.SetOwner($管理员组Sid) $访问控制列表.SetAccessRuleProtection($true, $false) $访问控制列表.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($系统Sid, "FullControl", "Allow"))) $访问控制列表.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($管理员组Sid, "FullControl", "Allow"))) Set-Acl -LiteralPath $授权密钥文件路径 -AclObject $访问控制列表 } catch { Write-Warning "已更新 authorized_keys 内容,但恢复严格 ACL 失败:$_" } } |