前阵子帮朋友调一台群晖,折腾完发现他还在用密码登录每一台服务器,我当时就有点绷不住。做运维或者自己玩服务器的人,SSH免密登录算是基本功中的基本功,可偏偏有太多人卡在“明明配了密钥还是不生效”这一步上。这篇东西不打算写成官方文档的复读机,就按我自己这几年从Windows、macOS、Linux到各种网络设备上折腾SSH免密的真实经历,把原理、配置、坑全部摊开讲一遍。不管是刚接触SSH的新人,还是已经被“Permission denied”折磨了一下午的老哥,应该都能在这里找到对应的解法。
1. 免密登录的底层逻辑:搞清楚密钥对是怎么“对上暗号”的
1.1 非对称加密在SSH里到底做了什么
SSH免密登录的核心不是“不需要密码”,而是把“人知道的密码”换成了“机器持有的密钥”。这里面用的是非对称加密体系,也就是一对钥匙:私钥和公钥。私钥留在你自己的客户端机器上,权限必须收紧,谁拿到它谁就能冒充你;公钥则是可以公开分发的东西,你把它放到目标服务器的 ~/.ssh/authorized_keys 文件里,服务器就认这把公钥了。
整个认证过程可以简化成三步:客户端发起连接时出示自己的身份信息,服务器从 authorized_keys 里找到对应的公钥,生成一个随机挑战值发给客户端;客户端用私钥对挑战值签名后返回;服务器用公钥验证签名。签名验证通过,连接直接建立,全程不需要输入密码。
这个机制的好处很明显——私钥不出本地机器,网络上传输的只有挑战值和签名,即使有人抓包也拿不到能用于冒充你的材料。相比之下,密码认证每次都要把密码送到服务器端比对,虽然SSH协议本身做了加密传输,但密码本身依然是一个“可被猜测、可被爆破”的弱点。我之前遇到过一台暴露在公网上的测试机,一天之内被尝试登录几千次,开了免密并关闭密码认证之后,日志立刻安静了。
1.2 为什么说免密不是“取消认证”,而是“更换认证方式”
很多人问我:免密登录是不是不安全?我的回答通常是:如果你把私钥管理好,免密登录比密码登录安全得多。真正要注意的,是别把私钥当成无所谓的东西随手乱放。私钥文件默认权限是600,也就是只有属主可读写,这个权限如果被改大了,SSH客户端会直接拒绝使用该私钥,这是保护机制在起作用。
另外要区分两种情况:一种是本机到本机的免密,比如用 ssh localhost 测试;另一种是跨机器的免密,比如从你笔记本SSH到云服务器。前者通常是为了在脚本里执行本地命令时不被打断,后者才是日常运维里最常见的场景。理解了“客户端持私钥、服务端存公钥”这个基本模型,所有配置思路都会变得非常清晰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 密钥生成与格式兼容:全平台实操细节
2.1 Linux和macOS下的密钥生成标准流程
在Linux或macOS的终端里生成密钥,我习惯用 ed25519 算法而不是老牌的 rsa。原因很简单:ed25519密钥更短、生成速度更快、安全性也足够,而且现代OpenSSH版本全都支持。命令如下:
bash复制ssh-keygen -t ed25519 -C "your_email_or_comment" -f ~/.ssh/id_ed25519
-C 参数是给密钥加一个备注,通常写自己的邮箱或者机器名,方便以后在 authorized_keys 文件里认出这是谁的密钥。-f 指定生成路径,如果想用默认路径,直接回车就行。执行过程中会提示设置私钥的密码(passphrase),这一步我建议设置,哪怕只是一个简单口令。设了passphrase之后,私钥文件即使被人拷走,没有口令也无法使用。
生成完成后,~/.ssh 目录下会出现两个文件:id_ed25519 是私钥,id_ed25519.pub 是公钥。公钥内容是一行文本,格式是 ssh-ed25519 一大串字符 备注,这一整行内容就是要放到服务器上的东西。
2.2 Windows平台的密钥生成与PuTTY格式转换
Windows用户的情况稍微复杂一点。Win10及以上的系统自带了OpenSSH客户端,可以直接在PowerShell或CMD里使用 ssh-keygen 命令,用法和Linux完全一致。但还有不少老用户习惯用PuTTY,PuTTY的密钥格式自成一套(.ppk),不能直接拿来给OpenSSH用,反之亦然。
如果你在Windows上用 ssh-keygen 生成了OpenSSH格式的私钥,但平时习惯用PuTTY登录,可以用PuTTYgen这个工具做一次转换:打开PuTTYgen,选择“Load”加载OpenSSH私钥,然后“Save private key”保存成 .ppk 格式。反过来,如果你手里只有 .ppk 私钥,而VSCode或其他工具使用的是OpenSSH格式,同样用PuTTYgen加载后再导出OpenSSH格式即可。
这里有一个我自己踩过的坑:PuTTYgen在转换或生成密钥时,如果Source框里没有足够的随机数据,点Generate按钮会卡住不动。解决办法是让鼠标在PuTTYgen窗口里来回晃动,直到进度条走完。这是程序设计如此,不是死机。
2.3 群晖、欧拉、麒麟等特殊系统的密钥生成注意点
标题里提到了群晖、欧拉(openEuler)、银河麒麟这些系统,它们底层其实都是Linux,SSH配置逻辑完全一致,只是某些发行版用了不同的权限管理方式或者默认配置。比如群晖的SSH功能需要在控制面板里手动开启,默认是关闭的;openEuler和银河麒麟这类国产系统,sshd的配置文件路径依然是 /etc/ssh/sshd_config,但默认可能启用了SELinux或额外的安全模块,导致密钥权限明明已经对了,还是登录失败。
遇到这种环境变量导致的奇怪问题,第一件事是看服务端日志。在大多数Linux系统上,执行:
bash复制journalctl -u sshd -n 50
或者查 /var/log/secure、/var/log/auth.log,日志里会直接告诉你被拒绝的原因,比盲目改配置高效得多。群晖的话,可以通过“日志中心”查看SSH相关的登录记录。
3. 从终端到图形客户端:几种常用方式的免密配置实操
3.1 终端命令行方式:手动把公钥放到服务器上
最通用、也最不容易出错的方法,是用 ssh-copy-id 脚本把公钥推送到服务器。命令如下:
bash复制ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
这个命令会要求你输入一次服务器密码,然后把指定的公钥追加到服务器的 ~/.ssh/authorized_keys 文件中,同时自动设置好相关目录和文件的权限。第一次使用可能会提示确认主机指纹,输入 yes 即可。
如果没有 ssh-copy-id(比如部分精简版系统没装这个工具),可以手动操作:先 cat ~/.ssh/id_ed25519.pub 复制公钥内容,然后SSH登录服务器,执行:
bash复制mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "这里粘贴公钥内容" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
三个权限缺一不可:~/.ssh 目录要是700,authorized_keys 文件要是600,用户的home目录也不能让其他用户有写权限。之前有台机器怎么配都免密失败,最后发现是home目录权限是775,调整成755后才恢复正常。
3.2 Xshell与Bitvise这类Windows客户端的配置要点
Xshell的用户在配置免密时有一个常见的困惑:Xshell的“用户密钥”管理工具里导入的密钥,和系统 ~/.ssh 目录下的密钥是两套逻辑。正确做法是,先在Xshell的“工具—用户密钥管理者”里生成或导入密钥,然后在“连接—用户身份验证”里选择“Public Key”方法,并指定对应的用户密钥。
Bitvise SSH Client的逻辑类似,但它的界面更直白一些:在“Client key manager”里加载私钥,登录时选择“publickey”认证方式即可。特别需要注意的是,如果你的服务器只允许密钥登录,而Xshell或Bitvise的私钥加载路径不对,会直接报“服务器拒绝了密码”或“No supported authentication methods available”,这时候优先检查客户端选中的认证方式是不是Public Key,而不是密码。
3.3 VSCode Remote-SSH与内置终端的免密联动
VSCode是现在很多人写代码的主力工具,Remote-SSH插件的免密配置本身不复杂,本质上就是让VSCode调用系统SSH客户端去连接服务器。只要你的终端里能免密登录,VSCode通常也能免密登录。但有几个小细节容易被忽略:
第一,VSCode的SSH配置读取的是用户级配置文件,Windows下是 C:\Users\你的用户名\.ssh\config,Linux/macOS下是 ~/.ssh/config。如果你想给某台服务器指定非默认私钥,需要在这个配置文件里写明。
第二,VSCode连接远程服务器后,内置终端会自动复用SSH连接,这时如果你在远程环境里执行 git push 或 ssh 到其他机器,密钥代理(ssh-agent)的配置就很重要了。最简单的方式,是在本地启动ssh-agent并把私钥加入,然后让VSCode通过 ForwardAgent yes 选项把代理转发到远程。这样远程机器上的Git操作也能直接使用本地私钥,省去在每台服务器上单独配密钥的麻烦。
第三,VSCode Remote-SSH偶尔会缓存旧的连接信息,导致明明密钥已经更新了,连上去还是提示失败。这时候执行“Remote-SSH: Kill VS Code Server on Host”命令,让它重新部署服务端组件就能解决。
3.4 Git、GitHub、GitLab与Gerrit的密钥配置逻辑
Git相关的SSH密钥配置,本质上和服务器登录是一模一样的,只是公钥存放的位置从服务器的 authorized_keys 文件变成了代码托管平台的“SSH Keys”设置页面。GitHub、GitLab、Gerrit、Gitee都有这个入口,把 id_ed25519.pub 文件内容粘贴进去保存即可。
配置完成后可以用 ssh -T git@github.com 测试连通性,如果看到类似 Hi 用户名! You've successfully authenticated 的输出,说明公钥已经被正确识别。这里有一个容易让新人懵的细节:GitLab和Gerrit不接受直接登录shell,它们的SSH账号只是用来做Git传输的,所以 ssh -T 返回非零退出码是正常现象,只要输出里出现了欢迎信息就算成功。
4. 服务端的安全加固与批量部署思路
4.1 如何禁止root直接SSH登录但保留管理能力
标题热词里有一条“如何取消使root用户也可以ssh登录”,这里得说清楚逻辑。几乎所有的安全基线都建议把 PermitRootLogin 设置为 no,杜绝root账号被直接爆破的可能。日常管理用普通用户登录,需要root权限时再用 sudo 提权,影响并不大。
如果你确实需要root远程登录,比如某些自动化脚本要求以root身份直连,可以把 PermitRootLogin 改成 prohibit-password,意思是仅允许使用密钥登录root,禁止密码登录。这个折中方案比完全放开好得多。改动配置后记得重启sshd服务,命令因系统而异:
bash复制systemctl restart sshd
# 或者
service sshd restart
在修改sshd配置时,强烈建议先保留一个已建立的SSH会话再重启服务,否则一旦配置写错,你可能会被锁在门外,只能去控制台或物理机上修改。这个教训我是真金白银换来的。
4.2 限制只有wheel组的用户可以远程登录
“设置只有wheel组的用户可以ssh远程登录”是一个很典型的加固需求。在sshd_config里加上一行:
conf复制AllowGroups wheel
重启sshd后,只有属于wheel组的用户才能通过SSH登录。如果某些普通用户也需要登录权限,要么把他们加入wheel组(但要考虑sudo权限也随之而来),要么在 AllowGroups 后面补充其他组名,用空格隔开。
这个配置在CentOS、Ubuntu、openEuler、银河麒麟上都能生效,因为核心机制在sshd层面,和发行版无关。需要注意的是,有些轻量发行版默认没有wheel组,你需要先用 groupadd wheel 创建,然后再把用户加进去:usermod -aG wheel 用户名。
4.3 批量分发公钥与known_hosts处理
如果手头有几十台机器要做免密,一台台敲 ssh-copy-id 太累了。批量场景下我一般会配合Ansible的 authorized_key 模块来做,把同样的公钥分发到所有目标机器。比如:
yaml复制- name: 分发SSH公钥
ansible.posix.authorized_key:
user: deploy
state: present
key: "{{ lookup('file', '/path/to/id_ed25519.pub') }}"
Ansible的优势在于幂等,重复执行不会重复追加公钥。如果没有Ansible,也可以写一个简单的Shell循环,但要注意 authorized_keys 的去重问题,否则重复执行会产生大量重复行,虽然没有致命危害,但会显得很乱。
批量连接时还有一个经典障碍:首次SSH连接会提示确认主机指纹。如果不想被这个交互卡住,可以在批量脚本里设置 StrictHostKeyChecking=no,但要注意这个选项会降低对中间人攻击的防护,只在初始化环境时临时使用,配完就改回来:
bash复制ssh -o StrictHostKeyChecking=no user@host
更规范的做法是先把所有主机的公钥指纹采集到 known_hosts 文件里,分发到各客户端机器,保持指纹校验不缺席。
5. 高频故障排查实录:从“拒绝密码”到“连接超时”
5.1 服务器提示“Permission denied (publickey)”怎么查
这可能是大家遇到最多的一条报错。排查顺序应该是:先确认服务器端 authorized_keys 文件里确实有你的公钥,再确认客户端当前连接用的是不是你放公钥对应的那把私钥,最后确认服务器端的sshd配置允许PublicKey认证。
实际操作中还有个容易忽略的点:如果用 ssh -v 详细模式连接,输出里会显示客户端尝试了哪些认证方式。如果客户端压根没尝试publickey,说明配置没指定密钥文件;如果尝试了但还是被拒绝,基本就是公钥不匹配或者权限问题。一条命令就能定位问题:
bash复制ssh -v user@server_ip
另外还要检查SELinux。在一些默认启用SELinux的系统上(比如CentOS、openEuler、银河麒麟),如果 ~/.ssh 目录的上下文标签不对,sshd会拒绝读取公钥文件。用 restorecon -R -v ~/.ssh 恢复一下标签,很多时候问题瞬间消失。
5.2 连接被拒绝或超时:不一定是密钥的问题
“ssh: connect to host github.com port 22: Connection refused”这类报错,在GitHub场景下多半是端口22被网络环境封了,解法是改用443端口连接GitHub。具体做法在 ~/.ssh/config 里添加如下配置:
conf复制Host github.com
Hostname ssh.github.com
Port 443
User git
如果是连自己服务器超时,优先检查安全组、防火墙规则和sshd服务状态。用 systemctl status sshd 看服务是否在运行,用 ss -tlnp | grep :22 看端口是否在监听,再用 iptables -L -n 或 firewall-cmd --list-all 检查防火墙放行情况。大部分连不上的问题,都出在这三层。
需要注意的是,华为交换机等网络设备也支持SSH登录,“华为交换机ssh配置”里的免密思路和Linux不一样,设备侧是在VTY视图下配置 authentication-mode publickey 或 authentication-mode aaa 并关联用户的公钥,操作界面完全不同。这类设备我建议查阅对应厂商的配置手册,不要直接套用Linux的逻辑。
5.3 客户端本地问题:私钥权限、ssh-agent与config配置
最后一类高频问题出在客户端。Windows下的OpenSSH对私钥权限要求很严格,如果 C:\Users\用户名\.ssh\id_ed25519 的权限继承了父目录的宽松设置,客户端会拒绝使用它。用管理员PowerShell执行:
powershell复制icacls C:\Users\用户名\.ssh\id_ed25519 /inheritance:r /grant:r 用户名:F
可以压缩权限。macOS上通常用 chmod 600 ~/.ssh/id_ed25519 就够了。
ssh-agent也是一个坑点。如果私钥设置了passphrase,每次使用都要输入一遍,太影响体验。把私钥加入ssh-agent后只需要在系统启动后输入一次:
bash复制eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
macOS上还可以用 ssh-add --apple-use-keychain 让私钥口令保存在钥匙串里,重启后不用重新添加。Windows的OpenSSH也有类似功能,通过服务管理ssh-agent,设置为自动启动后会自动加载默认密钥。
5.4 特定环境的疑难杂症速查表
为了让你在排障时少走弯路,我把这些年在各种场景下遇到过的典型问题整理成了速查表:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 群晖显示“SSH服务未开启” | 控制面板-终端机和SNMP里没启用 | 手动开启SSH功能 |
| Win10关机后再开机连不上免密 | ssh-agent服务未设为自动启动 | 服务管理器里设置并启动 |
| Git推送时提示权限不足 | 用错了邮箱生成的公钥 | 检查Git配置的邮箱与公钥备注 |
| VSCode连上后codex无法登录 | 远程环境缺少凭证或密钥代理未开启 | 配置ForwardAgent yes并重启远程连接 |
| 国产系统检测出“危险服务” | sshd服务对外暴露且允许密码登录 | 关闭密码认证+限制登录用户 |
| ascp传输报TCP连接失败 | 非SSH端口未放行 | 检查防火墙的UDP/TCP端口策略 |
| GitLab提示“You're using a password” | clone地址用了http而非ssh | 更换为SSH协议的clone地址 |
| 华为交换机做免密失败 | 设备端认证模式配置错误 | 登录设备检查VTY与SSH用户绑定 |
5.5 macOS与Android客户端的补充说明
macOS用户连接远程服务器时,如果提示“It is required that your private key files are NOT accessible by others”,先用 ls -l ~/.ssh/ 检查权限,把私钥文件统一调整成600,目录调整成700。
Android手机的SSH客户端(比如JuiceSSH、Termux)也可以做免密。Termux里的操作和Linux几乎相同,生成密钥后把公钥贴到服务器上;JuiceSSH则在“连接—认证”里导入私钥。这个场景适合手机管理服务器时省去输密码的麻烦,但手机上保存私钥有较大泄露风险,建议额外设置应用锁或系统加密。顺带提一句,“海纳斯ssh恢复出厂设置”这类问题,如果出现在终端设备上,一般是在服务端管理界面里重置SSH配置,具体路径看设备品牌,但核心都是清空密钥对并重新生成。
6. 把免密配置固化为一套可复制的方法论
写了这么多,最后聊点方法论层面的东西。SSH免密登录看似是一个小配置,但它牵扯到密钥算法选型、客户端与服务端格式兼容、权限管理、批量运维、故障排查等多个环节。我在实际项目里总结了一个通用流程:先在本地生成密钥对,再用 ssh-copy-id 或Ansible分发公钥,然后核实权限与SELinux标签,最后关闭密码登录并重启sshd,整个过程控制在十分钟以内。如果哪一步卡住,就用 ssh -v 加上服务端日志两边对照,基本没有解不了的问题。
我自己平时还有一个习惯,就是给不同的目标场景生成不同的密钥。比如个人GitHub用一把,公司GitLab用一把,服务器批量管理用一把,都放在 ~/.ssh/ 下,靠 config 文件指定对应关系。这样即使某一把私钥泄露,影响范围也能被限制住,不用全部重来。这个习惯救过我一次,背包被偷后第一时间吊销对应密钥,其他环境毫发无损。
关于SSH免密登录,能讲的内容其实还有很多,比如证书认证、跳板机、堡垒机联动这些进阶玩法。但只要你把今天这篇里的基础逻辑和排障思路掌握了,那些进阶方案理解起来就是顺水推舟的事。希望这篇从实战中总结出来的东西,能让你少踩几个我之前踩过的坑。
