ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Server 2016 安装 OpenSSH Server:在线/离线与公钥配置

Server 2016 安装 OpenSSH Server:在线/离线与公钥配置 1. 先把这件事看清楚Server 2016 装 OpenSSH Server 的难点在哪1.1 一个真实的管理场景我手上有一批 Windows Server 2016角色很杂有单纯跑 IIS 的有做文件共享的也有当 RDS 会话主机用的。日常工作里最烦的其实不是业务问题而是进不去机器这件事。远程桌面偶尔抽风、控制台需要现场、批处理脚本想定时拉取日志却只能用 WinRM 或者共享目录绕。后来我把 OpenSSH Server 统一装到了这些机器上情况就完全变了登录、传文件、跑命令、拉日志、批量巡检全都在一个终端里解决键盘敲下去就有反馈不用再等图形界面慢慢加载。Windows Server 2016 上装 OpenSSH Server说难不难说简单也真谈不上简单。难的从来不是那几条命令而是那些没人提前告诉你的坑服务注册上了却起不来、公钥塞进去还是 Permission denied、连上以后中文全是问号、默认 Shell 是 cmd 导致方向键和退格键难受得要命、给多台机器批量部署时脚本一跑就报权限错误。这些问题没有一个能在官方 README 里找到完整答案基本都是靠一次次试出来的。这篇文章面向的是真实在机房或者远程管服务器的人不管你是刚接手几台 Windows 服务器的新手还是已经用惯了 Linux 那一套、第一次在 Windows 上碰 sshd 的老运维都能照着走通。我会把在线装、离线装两条路线都写清楚然后把 sshd_config 里必须动的默认值、公钥认证的权限深水区、故障排查顺序、以及 RDS 场景下要多留的心眼全部摊开讲。代码和命令都是可以直接抄的参数我会解释为什么这么填。1.2 为什么 Add-WindowsCapability 这条路在 2016 上走不通如果你搜过相关资料大概率会看到这样一段命令Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0这段命令本身没错但它在 Windows Server 2016 上会直接告诉你找不到这个功能。原因很简单OpenSSH 作为系统可选功能按需功能FoD集成进系统是从 Windows 10 1809 和 Windows Server 2019 这一代开始的。Server 2016 的组件库里压根没有这个包你敲多少次都是一样的报错和权限、网络、系统语言都没关系。这个认知很重要因为它决定了你的技术路线。很多人卡在这一步以为是系统没打补丁、以为要装什么更新包然后开始折腾 Windows Update浪费一两个小时。实际上 Server 2016 只有一条正路用微软在开源仓库里维护的 Win32-OpenSSH 独立发布包。它是一套纯绿色的可执行文件加几个 PowerShell 脚本自带服务注册逻辑不依赖系统组件库也不需要联网激活什么功能。顺带说一句Server 2016 自带的是 Windows PowerShell 5.1这个版本足够跑 Win32-OpenSSH 的安装脚本不需要额外去装 PowerShell 7。但如果你打算在 SSH 会话里长期用更现代的语法和跨平台能力后面可以再补一个 PowerShell 7这个不影响 sshd 本身的运行。1.3 三条可选路线与取舍把可选方案摆在一起看选型逻辑其实很清晰路线适用场景优点需要注意的点在线拉取官方发布包服务器能访问外部网络或者有内部镜像源版本最新脚本最全一次成型需要走发布渠道下载内网机器要先搬运离线拷贝安装生产内网、隔离网段、无外网出口完全可控可以先用测试机验证版本包的完整性校验要自己做路径不能带中文第三方打包如包管理器已有统一软件分发平台与现有流程融合版本可能滞后脚本被改造过出问题不好对源码我个人的选择是新机器一律用官方发布包的离线版先在测试机上把整个流程跑一遍确认没问题后把同一个压缩包放进文件服务器所有生产机从文件服务器取。这样做的好处是版本绝对一致以后排查问题不会出现A 机器是 8.x、B 机器是 9.x、行为不一样这种尴尬情况。至于能不能直接从某台机器上把已经装好的目录拷到另一台技术上可行但我不推荐作为首选。原因是服务注册这一步注册表项、服务账户、权限设置不是拷文件就能带过去的你省下的那点时间后面调试服务起不来会加倍还回去。2. 动手前的环境体检清单2.1 系统与补丁基线确认正式动手前先花三分钟把机器摸清楚这三分钟能省掉后面很多无意义的排查。# 系统版本与内部版本号 Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsBuildNumber, OsArchitecture # 系统补丁最后安装时间判断这台机器的维护状态 Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10 HotFixID, InstalledOn # 是否为 64 位系统 [Environment]::Is64BitOperatingSystem作用是什么第一确认架构。Server 2016 基本都是 x64但如果你手上有一台老旧的 32 位环境包要换 Win32 版本脚本也不一样。第二确认补丁基线。一台半年没打过补丁的机器装完 sshd 之后可能因为加密算法支持问题连不上这种问题排查起来最耗时不如先确认机器处于正常维护状态。第三确认系统本身来源正规、授权正常。这一点不是技术问题但它是你后续所有变更的前提来路不明的系统不要拿来做生产入口。需要特别提醒的是如果你计划在装 OpenSSH 之前给服务器补丁请把两件事分开做中间隔一次重启和一轮业务验证。原因我后面在第 8 章会讲——补丁、重启、会话层状态这三样东西叠在一起的时候问题会变得非常难定位你分不清是 sshd 的问题还是系统本身的问题。2.2 PowerShell 与依赖检查# PowerShell 版本 $PSVersionTable # 执行策略安装脚本需要能跑 Get-ExecutionPolicy -List # 是否已经有人装过 sshd避免重复安装 Get-Service -Name sshd, ssh-agent -ErrorAction SilentlyContinue Get-Command sshd.exe -ErrorAction SilentlyContinueGet-ExecutionPolicy -List这一步特别重要。Win32-OpenSSH 的安装脚本是 PowerShell 脚本如果当前会话的执行策略是 Restricted脚本一跑就红字报错而报错信息往往被新手当成脚本有问题。实际上用powershell.exe -ExecutionPolicy Bypass -File .\install-sshd.ps1这种一次性绕过的方式就能解决不用去改机器的全局策略——改全局策略是给自己留隐患。再说检测是否已经装过。生产环境经常出现的情况是前任运维装过一半文件在但服务没注册或者服务在但配置文件被改乱了。这时候直接覆盖安装比从零开始更省事但你得先知道现状否则你会在为什么服务名已经存在这个报错上卡很久。2.3 端口、磁盘与权限预检# 22 端口是否被占用有些管理组件会偷偷监听 Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, State, OwningProcess # 反查占用进程 Get-Process -Id (Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue).OwningProcess -ErrorAction SilentlyContinue # 系统盘剩余空间 Get-PSDrive -PSProvider FileSystem | Select-Object Name, {nFreeGB;e{[math]::Round($_.Free/1GB,2)}} # 防火墙三个配置文件的状态 Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction22 端口被占用这件事在 Windows 上比在 Linux 上更常见因为某些监控组件、备份组件、远程管理工具会挑一个低位端口监听恰好撞上 22。如果你发现端口被占有两条路改 sshd 的监听端口或者让那个组件换端口。我一般选前者因为改动面更小只需要在 sshd_config 里加一行 Port再放行对应防火墙规则。磁盘空间这块OpenSSH 本体只占几 MB不用担心。真正要留意的是日志目录如果你把 LogLevel 开到 DEBUG3 排查问题日志量会暴涨排查完记得调回来。防火墙这块先看清楚三个配置文件Domain、Private、Public哪个是启用的。很多能 ping 通但连不上 22的问题根因就是入站规则加在了 Domain 配置文件上而服务器实际网络位置被识别成了 Public。3. 在线安装从零到 sshd 服务跑起来3.1 下载与解压目录的选择拿到官方发布包之后第一个要做决定的是装到哪。我的建议是固定在C:\Program Files\OpenSSH。理由有三一是这个路径不含空格以外的特殊字符Program Files 中间那个空格虽然有点烦但只要所有引用都加引号就没问题二是它在系统盘的默认权限体系里普通用户无法随手修改三是路径固定之后后面写批量脚本、写监控、写文档都不用再改。解压这一步有个非常容易被忽略的坑官方压缩包解开之后里面自带一层OpenSSH-Win64目录。也就是说如果你把压缩包直接解到C:\Program Files最终你会得到C:\Program Files\OpenSSH-Win64\sshd.exe。这时候你如果按文档去C:\Program Files\OpenSSH找脚本当然找不到。# 假设压缩包已经放在 C:\Temp\OpenSSH-Win64.zip $zip C:\Temp\OpenSSH-Win64.zip $dest C:\Program Files # 方式一Expand-Archive脚本里最直观 Expand-Archive -Path $zip -DestinationPath $dest -Force Rename-Item -Path C:\Program Files\OpenSSH-Win64 -NewName OpenSSH # 方式二文件数量多时快很多推荐在自动化脚本里用 Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, C:\Temp\Expand) Move-Item C:\Temp\Expand\OpenSSH-Win64 C:\Program Files\OpenSSH方式二比方式一快原因是 Expand-Archive 在 PowerShell 5.1 里是逐文件调 .NET 接口几百个小文件时会明显拖时间。这在批量部署几十台机器的时候差别很大一台省二十秒三十台就是十分钟。解压完之后先别急着跑脚本把目录内容过一眼Get-ChildItem C:\Program Files\OpenSSH | Select-Object Name, Length | Sort-Object Name你应该看到 sshd.exe、ssh.exe、ssh-keygen.exe、sftp-server.exe、scp.exe 这些核心文件以及 install-sshd.ps1。老一点的包里还会有 OpenSSHUtils 模块目录和 FixHostFilePermissions.ps1新版本已经把权限修复的功能合并进主脚本了。如果你拿到的是新包却看到网上教程让你去导入 OpenSSHUtils 模块那说明教程的版本对不上——遇到这种版本错配正确做法是整体换一个匹配的包而不是东拼西凑地把模块拷进去。3.2 执行安装脚本注册服务这一步是整个流程的核心。安装脚本干的事情是把 sshd 和 ssh-agent 注册成 Windows 服务设置好服务账户和启动参数并生成一些必要的注册表项和权限。cd C:\Program Files\OpenSSH powershell.exe -ExecutionPolicy Bypass -File .\install-sshd.ps1正常输出是类似sshd and ssh-agent services successfully installed的提示。如果这一步报错八成是三件事一是没用管理员权限开 PowerShell二是路径不对脚本根本没找到三是包版本太老脚本里的某些 .NET 调用在当前系统上跑不通。一个值得记住的经验每次装完先看看服务是否存在再谈能不能启动。这两个是不同性质的问题。Get-Service -Name sshd, ssh-agent | Select-Object Name, Status, StartType如果服务不存在是注册失败如果服务存在但启动报错是运行环境问题。把这两类问题分清楚排查效率会高很多。3.3 启动服务、放行端口、开机自启默认情况下安装脚本把两个服务设成手动启动这是有意为之的设计——不强制用户一装完就开。生产机器上我们肯定要设自动Set-Service -Name sshd -StartupType Automatic Set-Service -Name ssh-agent -StartupType Automatic Start-Service sshd Get-Service sshd, ssh-agent | Select-Object Name, Status, StartType关于 ssh-agent 这个服务网上有说法是公钥认证必须要它。这个说法不准确。ssh-agent 是密钥管家服务主要服务于客户端场景比如你在服务器上主动去连别的机器时不用反复输密码。服务端只用公钥登录的话它不是必须的。但我还是建议一起设成自动原因很简单以后你哪天要在服务器上做密钥转发、或者用它去拉备份再回头改启动类型容易出现配置改了但没人记得的情况。防火墙规则# 全放行适用于测试环境 New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName OpenSSH Server (sshd) -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 # 限制来源网段生产环境推荐这种 New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName OpenSSH Server (sshd) -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -RemoteAddress 10.20.0.0/16, 10.30.5.0/24我强烈建议生产环境直接用第二种。原因不是限制网段能防住谁而是它能把误连和扫描噪音挡在门外日志干净了你才看得清真正的异常登录。还有一个经常被忽略的动作把安装目录加进系统 PATH。$oldPath [Environment]::GetEnvironmentVariable(Path, Machine) if ($oldPath -notlike *OpenSSH*) { [Environment]::SetEnvironmentVariable(Path, $oldPath;C:\Program Files\OpenSSH, Machine) }不加 PATH 的后果是sshd_config 里的Subsystem sftp sftp-server.exe用的是相对路径服务本身因为在安装目录下运行所以能找到但你在命令行里敲 ssh-keygen、sftp 就得写全路径久而久之脚本里全是长路径既难读又容易出错。3.4 第一次登录验证与指纹确认服务起来了从另一台机器上验证ssh -p 22 -v 用户名10.20.1.30第一次连接会出现主机指纹确认提示。这一步别随手回车正确做法是把指纹和服务器上的实际指纹对一下# 在服务器上查看主机公钥指纹 Get-ChildItem $env:ProgramData\ssh\ssh_host_*_key.pub | ForEach-Object { $fp C:\Program Files\OpenSSH\ssh-keygen.exe -lf $_.FullName $($_.Name): $fp }对上了再确认这是防止中间人篡改的最基本一环。很多事故不是技术漏洞导致的而是顺手按了 yes。如果服务器上%ProgramData%\ssh目录里一个 host key 都没有服务通常会报no hostkeys available -- exiting.然后退出。解决办法就一条命令 C:\Program Files\OpenSSH\ssh-keygen.exe -A-A的参数含义是为所有缺失的密钥类型各生成一套比自己一条条敲 rsa、ecdsa、ed25519 省事得多而且不会漏。4. 离线安装内网机器的搬运方案4.1 包怎么带进去校验怎么做生产内网一般没有直接的对外出口所以包得先在外网机器上下载再通过文件服务器或者跳板机搬进去。这一步有个细节值得强调搬运之前先在本地校验一次哈希。Get-FileHash -Algorithm SHA256 .\OpenSSH-Win64.zip把哈希值记在变更单或者部署文档里。到了内网再算一次两个值一致才继续。为什么这么较真因为压缩包在多次拷贝过程中损坏的概率虽然低但一旦损坏症状通常是某些文件解不出来或者脚本跑一半报奇怪的错这类问题排查起来非常痛苦而校验哈希只要十秒钟。搬运路径也有讲究不要放在带中文或空格的目录下。我见过最典型的问题是压缩包放在某个用户桌面上桌面路径里带了中文用户名解压之后脚本读取路径时出现编码问题报错信息完全不知所云。统一放到C:\Temp这种干净路径下操作最省心。4.2 脚本执行策略的处理内网机器上常见的一种情况是执行策略被统一管控成了 AllSigned 或者 Restricted。这时候跑安装脚本会直接被拦。处理方式有两种我推荐第一种# 第一种只对这一次调用绕过不留痕迹 powershell.exe -ExecutionPolicy Bypass -NoProfile -File .\install-sshd.ps1 # 第二种临时改当前进程的策略会话结束就失效 Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass千万不要用Set-ExecutionPolicy -Scope LocalMachine Unrestricted图省事。这个改动是持久的而且会在安全基线检查里被标出来等于给自己挖坑。4.3 版本升级时怎么覆盖不丢配置OpenSSH 用久了肯定要升级比如为了支持更新的算法、或者修掉某个已公开的问题。升级的正确顺序是先停止服务Stop-Service sshd, ssh-agent备份配置把C:\ProgramData\ssh\sshd_config复制一份带日期后缀备份主机密钥%ProgramData%\ssh\ssh_host_*_key整个目录复制走用新包覆盖C:\Program Files\OpenSSH下的程序文件启动服务验证登录其中第 3 步是最容易被跳过、代价也最大的一步。主机密钥一换所有客户端都会弹出指纹变更警告某些做了严格校验的工具会直接拒绝连接你得挨个去清理 known_hosts规模大的时候非常烦。还有一个细节程序文件覆盖的时候如果 sshd 服务还在跑文件会提示被占用。这就是为什么第 1 步必须先停服务。我见过有人强行覆盖失败后只覆盖了成功的部分文件结果新旧版本混在一起服务起不来还找不到原因。值得注意的是新版本的包不会覆盖C:\ProgramData\ssh\sshd_config因为配置文件和程序文件是两个目录。所以你改过的配置会保留但同时也要留意新版可能引入了新的默认行为升级后建议把sshd -T的输出和升级前对比一下。5. sshd_config 逐项拆解哪些默认值必须动配置文件在C:\ProgramData\ssh\sshd_config。Windows 下的路径和 Linux 不同这一点先记住别去/etc/ssh找。改配置的通用流程是改完用sshd -T检查语法再重启服务。 C:\Program Files\OpenSSH\sshd.exe -T Restart-Service sshd-T会把生效后的完整配置打印出来包括那些你没写、用的是默认值的项。这个习惯非常重要——很多我明明改了却没生效的问题都是因为配置写在了错误的层级或者被后面的 Match 块覆盖了。5.1 端口、监听地址与协议相关项Port 22 # ListenAddress 0.0.0.0默认监听所有地址。如果这台机器有多块网卡一块走业务、一块走管理强烈建议用 ListenAddress 只监听管理网卡的地址。这样即使业务网段被扫SSH 也不会暴露。注意 ListenAddress 一旦显式指定默认的全地址监听就失效了需要几个地址就写几行。关于改端口这件事我的观点比较明确改端口不是安全措施只是降低日志噪音。它挡不住有针对性的扫描但能挡掉大部分无差别扫段。如果你们的日志告警体系很敏感天天被无效登录刷屏那么把端口换到一个高位端口是有实际收益的。改完别忘了同步改防火墙规则我见过好几次改了端口忘了改防火墙然后自己把自己锁在外面。5.2 认证相关PasswordAuthentication 与公钥PubkeyAuthentication yes PasswordAuthentication no PermitEmptyPasswords no MaxAuthTries 3 LoginGraceTime 30这几行是整个配置里最需要想清楚的。PasswordAuthentication no是安全上的最优解但一刀切容易出事万一你的公钥没配对密码又关了就只能去控制台救了。我的做法是分两步走。第一步先保持密码认证开着把公钥认证跑通确认每个需要登录的账号都能用密钥进。第二步再关密码认证而且关之前先保留一个应急的管理员账号用密码登录同时给这个账号配一个非常复杂的口令并限制来源网段。等所有客户端都切换到密钥方式再把这个应急口子也关掉。MaxAuthTries 3和LoginGraceTime 30是限制暴力尝试的两个参数。默认值偏宽松收紧一点没有副作用因为正常登录一次就够了不可能重试三次。5.3 默认 Shell 与 Subsystem sftpWindows 上 SSH 登录之后默认落到 cmd.exe这个体验说实话不太好方向键调不出历史命令、Tab 补全基本没有、翻页和某些控制字符显示异常。解决办法是通过注册表指定默认 ShellNew-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -PropertyType String -Force Restart-Service sshd这里有两个必须注意的点。第一这个注册表项只接受可执行文件的完整路径不要在后面加参数比如想加-NoLogo写了会被当成路径的一部分导致登录失败。第二如果你装了 PowerShell 7也可以指向 pwsh.exe效果更好但前提是它已经正确安装并且路径存在这个位置。sftp 子系统Subsystem sftp sftp-server.exe如果你已经把 OpenSSH 目录加进了系统 PATH这样写没问题。如果没加就写全路径用正斜杠或者双反斜杠Subsystem sftp C:/Program Files/OpenSSH/sftp-server.exesftp 通不通直接影响你后面传文件顺不顺。验证方式很简单从客户端执行sftp 用户名主机看能不能进交互界面。5.4 日志级别与调试开关LogLevel INFO排查阶段可以临时调到 VERBOSE 或 DEBUG3问题定位完立刻调回来。为什么强调立刻因为 DEBUG3 会把每次连接的内部细节全写进日志一台被扫的机器一天能产生几百 MB 日志把系统盘写满的情况我见过不止一次。日志的位置取决于包版本两种都查一下# 老版本走 Application 日志来源是 sshd Get-EventLog -LogName Application -Source sshd -Newest 20 -ErrorAction SilentlyContinue # 新版本走独立通道 Get-WinEvent -LogName OpenSSH/Operational -MaxEvents 30 -ErrorAction SilentlyContinue把这两条命令记住后面排查问题时会反复用到。6. 公钥登录的深水区权限与路径6.1 普通用户与管理员走的是两套文件这是 Windows 版 OpenSSH 和 Linux 版差异最大、也最容易踩坑的地方。普通用户的公钥放在C:\Users\用户名\.ssh\authorized_keys管理员组成员的公钥放在C:\ProgramData\ssh\administrators_authorized_keys关键在于如果一个账号属于 Administrators 组sshd 默认会去读administrators_authorized_keys压根不看用户目录下的那个文件。这个规则写在 sshd_config 的 Match 块里Match Group administrators AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys所以现象就是你明明把公钥正确放进用户的.ssh\authorized_keys权限也修好了登录还是 Permission denied。这时候别怀疑密钥去检查这个账号是不是管理员。6.2 icacls 权限修复的正确姿势Windows 版 sshd 对 authorized_keys 文件的权限检查比 Linux 严格得多。原因是 Windows 的权限继承很复杂如果一个文件的权限能被其他用户改写那这个文件就不能作为认证依据——别人改一下就能登录你的账号。所以 sshd 会校验这个文件只能被该用户和 SYSTEM 访问。管理员文件修复$ak $env:ProgramData\ssh\administrators_authorized_keys icacls.exe $ak /inheritance:r icacls.exe $ak /grant Administrators:F icacls.exe $ak /grant SYSTEM:F普通用户文件修复$ak $env:USERPROFILE\.ssh\authorized_keys icacls.exe $ak /inheritance:r icacls.exe $ak /grant $($env:USERNAME):F icacls.exe $ak /grant SYSTEM:F/inheritance:r的作用是切断父目录的权限继承这一步是必须的不切断的话父目录上的 Users 组权限会继承下来校验就不过。顺手把 .ssh 目录本身的权限也收一下icacls.exe $env:USERPROFILE\.ssh /inheritance:r icacls.exe $env:USERPROFILE\.ssh /grant $($env:USERNAME):F icacls.exe $env:USERPROFILE\.ssh /grant SYSTEM:F有一点需要说明不同版本的 sshd 对权限检查的严格程度有细微差异有的版本会明确告诉你哪个权限不对有的版本只给你一句 Permission denied。所以修完之后用ssh -v看客户端输出再用服务端日志对照是最快的定位方式。6.3 authorized_keys 常见坑BOM、换行、注释权限修好了还是连不上那大概率是文件内容的问题。有三个坑我全都踩过第一个是UTF-8 BOM。用Set-Content -Encoding UTF8写文件在 PowerShell 5.1 里会写入 BOM 头也就是文件开头的三个隐藏字节。sshd 读到这三个字节第一行公钥就解析失败了。正确写法是明确指定不带 BOM$key ssh-ed25519 AAAAC3Nza... 运维笔记本 $ak $env:ProgramData\ssh\administrators_authorized_keys [System.IO.File]::WriteAllText($ak, $key rn, (New-Object System.Text.UTF8Encoding($false)))第二个是换行符。Windows 用 CRLFLinux 用 LF。Windows 版 sshd 两种都能处理但如果你是从 Linux 那边拷文件过来、中间经过了文本模式传输可能会出现行尾混杂极端情况下把两行公钥连成一行。建议一条公钥一行多余的空白和换行清干净。第三个是把注释当命令。authorized_keys 每行开头可以用#注释但注释必须在行首不能写在公钥后面另有含义公钥本身的行尾部分最后那个字段才是备注随便写中文英文都行不影响认证。7. 安全加固把它当成生产入口来对待7.1 关闭密码认证的过渡策略前面提过密码认证要关但不能一刀切。这里给一个我实际用过的过渡方案。第一步先在 sshd_config 里保持密码认证开启同时打开详细日志PasswordAuthentication yes PubkeyAuthentication yes LogLevel VERBOSE第二步把所有需要登录的账号都配好公钥逐个验证。验证的时候用ssh -o PreferredAuthenticationspublickey强制走公钥确保不是靠密码悄悄进去的。第三步观察一到两周日志确认公钥登录稳定、没有遗漏的账号。第四步关闭密码认证同时把应急账号的来源限制写进防火墙规则PasswordAuthentication noLogLevel VERBOSE在这里的价值是它会记录每次认证用了什么方式、哪个密钥指纹。日志里如果还有大量password记录说明有客户端还在用密码这时候贸然关闭会导致业务中断。7.2 限制来源与登录对象除了防火墙层面的来源限制sshd_config 层面还能再做一层收口AllowGroups SSH Users DenyUsers AdministratorAllowGroups是白名单机制只有指定组的成员才能登录。这个做法比在每台机器上维护用户列表更容易管理尤其是在域环境里——建一个域安全组需要 SSH 权限的人加进去不需要的移出来权限变更一个地方搞定。需要提醒的是AllowGroups一旦启用所有不在组里的账号都会被拒包括你常用的管理员账号。所以配置之前先确认自己在组里否则重启服务之后你会发现自己被关在门外。这种自己把自己锁了的事故在有控制台的情况下还能救纯远程环境下就得走带外管理非常麻烦。7.3 日志审计与异常登录发现日志不拿来看就是白记。我一般会配一个简单的巡检任务每天扫一遍登录失败记录# 统计最近 24 小时的失败认证来源 $since (Get-Date).AddHours(-24) Get-WinEvent -LogName OpenSSH/Operational -MaxEvents 2000 -ErrorAction SilentlyContinue | Where-Object { $_.TimeCreated -gt $since -and $_.Message -match Failed|Invalid } | Group-Object { ($_.Message -split n | Select-String -Pattern from\s(\d\.\d\.\d\.\d)).Matches.Groups[1].Value } | Sort-Object Count -Descending | Select-Object -First 10 Name, Count这条命令的效果是把失败次数最多的来源 IP 排出来。如果一个来源在短时间内有几百次失败那基本就是扫描或者暴力尝试直接加到防火墙黑名单里就行。配套的建议是把 SSH 端口只对内网管理网段开放日志量会下降一个数量级异常才看得清楚。日志淹没在噪音里等于没有日志。8. 故障排查速查表连不上、起不来、乱码8.1 服务侧的排查顺序服务起不来的时候按这个顺序走不要跳步# 1. 服务状态和最后一次错误 Get-Service sshd | Select-Object Name, Status, StartType Get-WinEvent -LogName System -MaxEvents 50 | Where-Object { $_.Message -match sshd } # 2. 配置语法 C:\Program Files\OpenSSH\sshd.exe -T # 3. 主机密钥是否存在 Get-ChildItem $env:ProgramData\ssh\ssh_host_* # 4. 端口是否被占 Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue # 5. 停掉服务前台跑一次看实时输出 Stop-Service sshd C:\Program Files\OpenSSH\sshd.exe -d第 5 步是最有价值的。-d让 sshd 在前台运行并打开调试输出所有启动过程中的错误都直接打在屏幕上比翻日志快得多。注意必须先停服务否则端口冲突会报错那个报错是假象会让你误判。现象最可能的原因处理方式服务启动后立刻停止主机密钥缺失ssh-keygen -A生成全部密钥服务完全无法启动配置文件语法错误sshd -T检查看报错行号启动报端口占用22 被其他组件监听换端口或让占用方让出服务在但连不上防火墙未放行或网段不符检查入站规则和 RemoteAddress连上后立刻断开默认 Shell 路径错误检查 DefaultShell 注册表值登录提示 Permission denied (publickey)权限或文件路径不对检查是否管理员账号、修 icacls用密码也进不去密码认证被关闭或账号无密码检查 PasswordAuthentication、账号口令8.2 客户端侧的排查顺序服务端没问题就从客户端看ssh -vvv -p 22 用户名10.20.1.30-vvv会打印完整的握手和认证协商过程。重点看三处一是协商出的算法列表如果两边没有交集会明确报出来二是认证方式的顺序和结果看服务器允许哪些方式三是最终失败的具体原因。一个常见的误判是客户端提示拒绝连接但实际原因是网络不通。先在客户端Test-NetConnection 10.20.1.30 -Port 22把连通性和应用层问题分开。Test-NetConnection -ComputerName 10.20.1.30 -Port 22 -InformationLevel Detailed8.3 中文乱码与终端体验优化这是我自己踩得最深的一个坑。现象是SSH 登录之后执行带中文输出的命令客户端看到的全是问号或者方块。根因是中文版 Windows 的默认控制台代码页是 GBK936而 SSH 通道按 UTF-8 处理两边对不上。按代价从轻到重有三个方案。轻量方案在 PowerShell 的 profile 里加上编码设置# 写入 $PROFILE当前用户对当前用户的所有 PowerShell 会话生效 [Console]::OutputEncoding [System.Text.Encoding]::UTF8 $OutputEncoding [System.Text.Encoding]::UTF8这个方案的优点是不需要重启不影响其他程序。缺点是对 cmd 里跑的原生程序输出不一定管用。中等方案把默认 Shell 换成 PowerShell 7如果装了它在编码处理上比 5.1 现代得多。重量方案开启系统的 UTF-8 全局支持区域设置里的 Beta 选项。这个方案要慎用它会让部分老程序读取中文路径和配置文件时出错尤其是年代久远的国产行业软件。如果这台机器上有关键业务系统别动这个开关。另外提一句把 Win32-OpenSSH 升级到较新的版本编码表现会明显改善。所以如果你的乱码问题很顽固先确认一下包版本是不是太老这可能比折腾编码设置更直接。终端体验方面除了前面说的把默认 Shell 换成 PowerShell还可以给客户端配一下。用 Windows 自带的终端或者成熟的 SSH 客户端都比老的命令行窗口舒服。字体选支持中英文等宽的中文不会挤在一起。8.4 RDS 与多会话场景下的注意事项RDS 会话主机上的情况要单独说因为这类机器的运维逻辑和普通服务器不一样。第一从我的实际观察看SSH 登录走的是独立的会话通道不经过远程桌面会话层因此不会占用远程桌面授权名额。这一点在应急时候很有用当会话数量已经吃紧、或者图形通道因为各种原因连不上时你还有一条通道能进去看日志、看服务、抓现场信息。第二补丁重启之后出现的各种会话异常提示很多时候根因在会话层和授权层跟 OpenSSH 没有关系。这类问题的正确处理方式是从后台通道进去查事件日志、确认相关服务状态把现场信息留全然后再决定后续动作。靠反复重启碰运气是最差的做法因为重启会覆盖掉很多有用的状态信息。同时也要清楚授权相关问题最终还是要走正规的解决途径不要试图用技术手段绕过。第三RDS 主机上的资源是共享的。SSH 会话会消耗内存和 CPU虽然单个会话的开销不大但如果有人写了一堆并发连接去跑脚本几十个会话同时起来对会话主机上的用户体验会有可感知的影响。我的做法是给运维脚本加上并发限制一般控制在 8 到 16 之间够用且不会把机器压垮。第四多台会话主机做统一部署时建议用统一的分发流程不要一台台手工装。手工装的结果一定是配置漂移三台机器三种 sshd_config出问题的时候你还得先搞清楚每台的配置到底长什么样。9. 批量落地的自动化思路与巡检脚本9.1 一段可复用的部署脚本如果只有三五台机器手工装也无所谓。到十台以上就该写成脚本了。下面这段是我实际用过的结构可以按需裁剪# 在管理机上执行依赖 WinRM 可达 $hosts Get-Content .\ssh_hosts.txt $pkg \\fileserver\deploy\OpenSSH-Win64.zip $sb { param($pkgPath) $dest C:\Program Files $tmp C:\Temp\OpenSSH-Win64.zip # 拷贝并校验 Copy-Item $pkgPath $tmp -Force $hash (Get-FileHash $tmp -Algorithm SHA256).Hash Write-Output PKG_HASH$hash # 解压 Add-Type -AssemblyName System.IO.Compression.FileSystem if (Test-Path C:\Temp\Expand) { Remove-Item C:\Temp\Expand -Recurse -Force } [System.IO.Compression.ZipFile]::ExtractToDirectory($tmp, C:\Temp\Expand) # 落位 if (Test-Path C:\Program Files\OpenSSH) { Stop-Service sshd -ErrorAction SilentlyContinue Stop-Service ssh-agent -ErrorAction SilentlyContinue } else { Move-Item C:\Temp\Expand\OpenSSH-Win64 C:\Program Files\OpenSSH } # 注册并启动 Push-Location C:\Program Files\OpenSSH powershell.exe -ExecutionPolicy Bypass -NoProfile -File .\install-sshd.ps1 Pop-Location Set-Service sshd -StartupType Automatic Start-Service sshd # 防火墙 if (-not (Get-NetFirewallRule -Name OpenSSH-Server-In-TCP -ErrorAction SilentlyContinue)) { New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName OpenSSH Server (sshd) -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -RemoteAddress 10.20.0.0/16 } Get-Service sshd | Select-Object MachineName, Name, Status, StartType } Invoke-Command -ComputerName $hosts -ScriptBlock $sb -ArgumentList $pkg -ThrottleLimit 8几个使用要点一是-ThrottleLimit 8控制并发别调太高WinRM 通道本身也会被压二是脚本里带了哈希输出跑完把每台机器的哈希对一遍确认版本一致三是覆盖安装的分支里先停服务再动文件这是前面说过的坑写在脚本里就不会忘。如果目标机器是隔离网段、WinRM 也不通那就退化成登录一次、跑一次脚本的方式把脚本放在文件服务器上通过计划任务或者手工执行。虽然笨一点但比手工改配置可靠。9.2 部署后的巡检项装完之后我一般用一个体检脚本确认状态把这些项目固化下来$result [ordered]{} # 服务状态 $svc Get-Service sshd -ErrorAction SilentlyContinue $result[ServiceStatus] $svc.Status $result[ServiceStart] $svc.StartType # 监听端口 $listen Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -eq 22 } $result[Listening] if ($listen) { Yes } else { No } $result[ListenAddress] ($listen.LocalAddress -join ,) # 版本 $result[Version] ( C:\Program Files\OpenSSH\ssh.exe -V 21) -join # 主机密钥 $result[HostKeys] (Get-ChildItem $env:ProgramData\ssh\ssh_host_*_key -ErrorAction SilentlyContinue).Count # 配置中的关键项 $cfg Get-Content $env:ProgramData\ssh\sshd_config -ErrorAction SilentlyContinue $result[PasswordAuth] ($cfg | Select-String ^\s*PasswordAuthentication).Line $result[PubkeyAuth] ($cfg | Select-String ^\s*PubkeyAuthentication).Line # 管理员公钥文件权限是否合规 $ak $env:ProgramData\ssh\administrators_authorized_keys $result[AdminKeyExists] Test-Path $ak if (Test-Path $ak) { $result[AdminKeyAcl] (icacls.exe $ak) -join | } [pscustomobject]$result | Format-List这个脚本的价值在于把我觉得应该没问题变成我确认过这些项目。特别是主机密钥数量和权限 ACL 这两项肉眼是看不出来的只能靠脚本查。跑完如果发现 PasswordAuth 是 yes而你的基线要求是 no那就说明这台机器漏配了。这种差异在几十台机器的环境里非常常见靠人工比对基本不可能发现。最后分享一个我在多机环境里养成的小习惯把每台机器的 SSH 配置差异做成一份清单每次巡检后更新。哪台机器因为特殊原因必须保留密码认证、哪台机器的端口做过调整都记下来。等技术交接的时候这份清单比任何文档都值钱——接手的人最怕的不是复杂而是没人知道为什么这台跟别人不一样。
返回列表