深夜往服务器上部署代码,屏幕冷冰冰地弹出一行Permission denied, please try again.,那一刻血压确实容易上来。输入三次密码全部被拒,检查键盘大小写没毛病,IP 也没拼错,最后折腾半天才发现是云控制台的安全组根本没放行 22 端口。这种经历我跟身边同事聊起来,几乎人人都踩过类似的坑。ssh 远程服务器报permission denied算是运维生涯中最高频的报错之一,但坑点从来不只在密码本身。这篇文章把我在生产环境里排查这个问题的完整思路、踩过的坑和能直接抄的作业整理出来,希望对正在被 SSH 连接折磨的人有点帮助。
1. 先弄清你碰到的是哪类 Permission denied 报错
很多人一看到permission denied就条件反射去改密码,结果折腾半天发现方向错了。实际上这条报错在不同阶段出现,背后原因完全不同。先把报错类型分清楚,比盲目试密码重要得多。
1.1 三类最常见报错的真实含义
SSH 连接过程中,permission denied主要出现在两个阶段:认证协商阶段和登录验证阶段,二者的报错文字有细微区别,含义却截然不同。
第一类最常见,也是新手最熟悉的:
Permission denied, please try again.这通常出现在密码认证过程中,服务端已经确认了用户名存在,但密码不匹配,于是提示"请再试一次"。如果连续输错三次,SSH 客户端会直接退出,并补一句Permission denied (publickey,password).。注意这里有个隐藏信息:只要报了这个错,说明网络层面已经通了,sshd 进程也活着,问题被限定在认证环节。
第二类长这样:
Permission denied (publickey).这行报错说明服务端拒绝了所有公钥认证尝试。一般是密钥不匹配、authorized_keys 文件内容有误,或者客户端没有提供正确的私钥。如果服务端同时禁用了密码认证,那你就彻底被挡在门外。这类问题在配置免密登录时最常出现,后文会展开讲。
第三类比较容易误导人:
Permission denied (publickey,password).注意括号里列出了两种认证方式,说明服务端允许使用公钥和密码,但都被客户端拒绝了。这种情况要分两层看:如果你明明用的是密码,说明用户名错了,或者服务端打开了某些限制(比如仅允许特定用户);如果你用的是密钥,那就要检查客户端到底发了哪个私钥、服务端 authorized_keys 里到底有没有对应公钥。
1.2 容易被忽视的伪 Permission denied 情况
还有一种报错虽然写的是Host key verification failed而不是permission denied,但在实际体验中经常让人误以为是权限问题,而且新手很容易把二者搞混:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@这段信息的意思是,客户端记录在~/.ssh/known_hosts里的服务器指纹,和现在连上的服务器指纹对不上。常见原因是服务器重装了系统、更换了 sshd 密钥,或者你自己重置过云主机,更危险的情况是遭遇了中间人攻击。如果确认服务器确实重新初始化过,清理 known_hosts 里的旧记录就能解决:
ssh-keygen -R 你的服务器IP我遇到过最离谱的情况是,服务器做迁移后 IP 被新机器复用,同事排查了整整一个下午的密码和密钥,最后才注意到是known_hosts里的旧指纹在捣乱。顺带说一句,如果你使用 VSCode 连接 SSH 远程服务器,这类 known_hosts 问题也会直接导致 Remote-SSH 插件反复连接失败,日志里出现的还是同一串警告。
注意:不要一上来就执行
rm -rf ~/.ssh/known_hosts。把整个文件删掉虽然省事,但等于放弃了 SSH 对服务器身份的验证能力,以后中间人攻击你不会收到任何提醒。用ssh-keygen -R精准清理指定主机才是安全做法。
2. 从网络到认证:一条链路逐层排除
搞清楚报错类型之后,接下来要做的就是沿着网络、服务、认证这条路一层层排除。我习惯把这个过程叫"剥洋葱",每次排查到底部一定是某个配置细节在作怪。
2.1 端口与网络层面排查
报错出现之前,先确认服务器真的能连通。很多权限问题其实是伪装的网络问题,比如安全组、防火墙把端口静默丢弃了,客户端反复重试后报超时,或者在特定场景下回你一个认证失败。
最简单直接的办法:
# 检查端口是否可连通,timeout 设为 5 秒 nc -vz -w 5 你的服务器IP 22正常情况输出类似:
Connection to 你的服务器IP port 22 [tcp/ssh] succeeded!如果提示Connection refused,说明 sshd 根本没在运行,或者监听端口不是 22;如果直接卡住无响应,大概率是防火墙把入站流量丢弃了。看宝塔面板、云安全组、本地 iptables 三个地方,大多数端口问题都出在这里。
提示:如果你用的是云服务器,安全组放行 22 端口属于最基础的步骤。我曾经帮人排查一台"怎么连都密码错误"的服务器,最后发现安全组只放行了 3389 端口,22 端口根本没开。但那台机器是 Windows 服务器,SSH 服务反而没有开启,整件事就是一场误会。
还有一个场景经常被忽略:公司网络或校园网出口封锁了非标准端口。我这个项目就遇到过客户现场防火墙只允许出站访问 80/443,22 端口根本出不去。这时需要把 SSH 服务迁移到 443 或 80 端口上,才能正常连上服务器。遇到Permission denied之前先想想你的网络环境是否对端口有特殊限制。
2.2 用户名、密码与认证方式排查
网络层的坑排完,再来说认证阶段。很多开发者在本地习惯了某个用户名,但服务器上创建的用户不一定同名,root 用户的 SSH 登录也可能被服务端禁止。
排查的时候,先在本地确认一个事情:
whoami ssh -v 用户名@服务器IP-v参数会打印详细的连接过程,其中有一行非常关键:
debug1: Authentications that can continue: publickey,password这一行告诉你服务端接受哪些认证方式。如果只有publickey没有password,那说明服务端禁用了密码认证,你输入多少次密码都没用。反之如果只有password,那就是公钥认证被关闭。看到这个输出,就能精确判断问题出在哪一侧。
用户名权限对不上也是高频场景。比如你用root@服务器IP去连,但服务端 sshd_config 里设置了PermitRootLogin no,输出的报错同样是Permission denied (publickey,password).。实际运维中 sshd 不允许 root 直接登录是很多企业的标准安全基线,需要先用普通用户登录再 su 切换。
2.3 服务端 sshd_config 关键参数解读
SSH 服务端的核心配置文件是/etc/ssh/sshd_config,几个直接影响认证结果的参数需要重点关注:
| 参数 | 取值 | 含义 |
|---|---|---|
PasswordAuthentication | yes/no | 是否允许密码认证 |
PubkeyAuthentication | yes/no | 是否允许公钥认证 |
PermitRootLogin | yes/prohibit-password/no | 是否允许 root 登录 |
MaxAuthTries | 数字 | 单次连接允许的最大认证尝试次数 |
PermitEmptyPasswords | yes/no | 是否允许空密码登录 |
AuthorizedKeysFile | 文件路径 | 指定公钥存放位置 |
StrictModes | yes/no | 是否检查用户文件和目录权限 |
这些参数看起来简单,但排列组合起来能产生很多让人摸不着头脑的问题。曾经帮人排查一台服务器,密码明明是对的,但一连接就被踢,-v调试输出显示服务端反复要求重新认证,最后发现问题出在MaxAuthTries设置为 2,而他本地的 SSH 客户端先尝试了公钥认证失败又继续尝试密码认证,两次失败之后直接被踢出,根本轮不到第三次输入正确密码。
修改完 sshd_config 之后一定要重启服务,而且要先重载再重启,避免语法错误把服务搞挂:
# 先检查配置语法 sshd -t # 再优雅重启 systemctl reload sshdsshd -t是每个运维都应该养成的习惯,改动任何配置之前先验证,能避免很多低级灾难。
2.4 防火墙、SELinux 等外部拦截因素
Linux 服务器上,除了 SSH 自身的认证逻辑,外部的访问控制机制也可能产生奇奇怪怪的"假认证失败"。
先看 SELinux。虽然现在大多数云镜像默认关闭了 SELinux,但有些企业自建机房的系统仍然开启。SELinux 开启时,如果 SSH 相关的布尔值不对,公钥认证可能被拒绝。检查方式:
getenforce # 如果输出 Enforcing,执行下面的命令查看 ssh 相关的规则 seinfo -b | grep ssh # 临时开放 ssh 相关规则 setsebool -P ssh_sysadm_login on还有/etc/hosts.deny和/etc/hosts.allow这套古老的 TCP Wrapper 机制。有些旧系统上,如果/etc/hosts.deny里写了sshd: ALL,而 hosts.allow 又没有匹配到你所在 IP,那 SSH 连接总是会表现异常的诡异。这类问题最麻烦的点在于,报错信息并不统一,有时是超时,有时是认证失败。
iptables 层面也会存在类似问题。INPUT链上如果有规则丢弃了来自某些 IP 段的 TCP 连接,客户端通常会表现为超时,但某些特殊场景下(比如 nftables 配合 conntrack 状态异常)也可能表现为认证阶段卡死或直接被服务端断开。排查时可以临时清空 iptables 规则测试,确认是防火墙问题后再把规则放回去:
iptables -F systemctl stop firewalld注意:这条命令只建议在测试环境或者你能物理访问服务器的情况下执行,生产环境清了 iptables 等于裸奔,出问题后果很严重。强烈建议先
iptables -L -n看一下规则,再决定要不要动。
3. 密钥免密登录失败的九个坑
如果说密码认证阶段的Permission denied还算好排查,那么密钥免密登录的失败就复杂多了。密钥认证涉及客户端和服务端两边的文件、权限、格式,任何一环不对都会导致Permission denied (publickey)。
3.1 密钥与目录权限不对,最常见的元凶
这是我在实际运维里遇到最多的一类问题。SSH 对权限有严格要求,原因在于如果.ssh目录或者私钥文件权限过宽,其他用户就能读取你的私钥,那公钥认证也就失去了意义。服务端为了保证安全,会直接拒绝权限不正确的密钥。
正确的要求如下:
# 客户端侧 chmod 700 ~/.ssh chmod 600 ~/.ssh/id_rsa chmod 644 ~/.ssh/id_rsa.pub # 服务端侧 chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chmod 700 ~ # 用户家目录不能是 777 等过高权限有人图省事,把.ssh目录设置成777,结果公钥认证永远失败。如果你在服务端tail -f /var/log/auth.log,能看到类似这样的记录:
Sep 18 12:33:41 host sshd[12345]: Authentication refused: bad ownership or modes for directory /home/deploy/.ssh日志里直接告诉你权限有问题,连服务器都不需要再解释更多。我处理过太多类似的案件,有些甚至是从网上复制了一键配置脚本,脚本里写的目录权限就有问题,复制下来直接踩坑。
实操心得:在维护大量服务器时,建议将这段权限修正命令写进初始化脚本里,每次创建完用户和密钥自动执行。避免靠记忆去敲 chmod,人总会在深夜犯错误。
3.2 authorized_keys 文件内容有问题
authorized_keys的内容要求非常死板:每一行一个公钥,以ssh-rsa、ssh-ed25519或ecdsa-sha2-nistp256开头,最后一般是注释信息(通常是用户名@主机名,也可以不加)。格式不对,公钥认证就会失败。
我遇到过一个比较刁钻的案例:公钥是用 Windows 记事本编辑后粘贴到 Linux 服务器上的,文件里的换行符变成了\r\n(CRLF),导致 sshd 把整个文件当成一个完整的 key 来解析,结果校验失败。处理方式很简单,用dos2unix转一下换行符:
dos2unix ~/.ssh/authorized_keys还有一种情况是向 authorized_keys 追加公钥时,没有使用追加模式。很多人执行:
echo "公钥内容" > ~/.ssh/authorized_keys如果此前文件里已经有其它公钥,这行命令会直接清空旧内容。多个运维共管一台服务器时,这个问题频繁发生。应该用>>追加,而不是>覆盖:
echo "公钥内容" >> ~/.ssh/authorized_keys不过我更推荐使用ssh-copy-id这个工具,它会自动处理权限、换行和目录创建:
ssh-copy-id -i ~/.ssh/id_rsa.pub 用户名@服务器IP这个命令会帮你把公钥追加到远端服务器的 authorized_keys,并自动设置好所有权限。新手用它基本不会出错。
3.3 家目录权限过宽的隐形雷
这个坑比.ssh目录的权限问题更隐蔽。很多人只盯着.ssh和authorized_keys的权限,忽略了用户家目录本身的权限。实际上 SSH 服务端的StrictModes安全检查会检验从家目录到authorized_keys整条路径上每一层的权限。
举个例子,如果用户家目录/home/deploy的权限是777,那么StrictModes会直接拒绝认证,因为任何人都可以篡改该目录下的authorized_keys或者写入恶意的公钥。
Sep 18 12:35:22 host sshd[12367]: Authentication refused: bad ownership or modes for file /home/deploy/.ssh/authorized_keys我的排查习惯是,凡是用密钥登录失败的机器,先整体跑一遍权限检查:
ls -ld /home/用户名 ls -ld /home/用户名/.ssh ls -l /home/用户名/.ssh/authorized_keys权限合理应该是:
drwx------ 2 deploy deploy 4096 Sep 18 12:00 /home/deploy drwx------ 2 deploy deploy 4096 Sep 18 12:00 /home/deploy/.ssh -rw------- 1 deploy deploy 800 Sep 18 12:00 /home/deploy/.ssh/authorized_keys家目录也可以是755,不一定非得 700,但绝对不能是777或者组内可写。尤其是后者,组内可写是安全配置规范中明确要求避免的。
3.4 ssh-agent 缓存与多个密钥共存导致“发错钥匙”
本地如果有多对密钥,SSH 客户端在默认情况下会逐个尝试。如果你用的不是默认文件名(比如~/.ssh/id_rsa、~/.ssh/id_ed25519),那客户端根本不会主动使用你的密钥,除非在配置里显式指定,或者用ssh-add将密钥加入 agent。
我遇到过一位同事,服务器只允许某些密钥登录,但本地同时存在一个旧的默认密钥和一个新生成的密钥。服务器端的authorized_keys只放新公钥,可客户端每次连接时,总是先用旧的默认私钥去尝试认证,失败后再用新私钥,本来也能通,但服务器MaxAuthTries只有 2,旧密钥失败一次、新密钥再失败一次,整体技术栈可能就超限了。解决方式是显式指定:
ssh -i ~/.ssh/custom_key 用户名@服务器IP如果你的密钥已经被 ssh-agent 缓存了,并且 agent 里存在多个密钥,那 SSH 的顺序还会受 agent 的影响。排查时可以用这些命令看看 agent 里到底缓存了哪些密钥:
ssh-add -l # 清空 agent 缓存 ssh-add -D # 只添加指定的密钥 ssh-add ~/.ssh/custom_key3.5 客户端 Config 文件与 IdentityFile 的影响
对于手动维护大量服务器的人来说,~/.ssh/config几乎是刚需。它能把复杂的连接参数固化下来,比如:
Host myserver HostName 192.168.1.100 Port 2222 User deploy IdentityFile ~/.ssh/deploy_key ServerAliveInterval 60 ServerAliveCountMax 3但 config 配置也有一个副作用:文件中配置的IdentityFile如果不存在,SSH 会静默跳过这个密钥。很多人以为配置了 IdentityFile 就一定生效,其实如果写错路径,客户端根本读不到私钥,然后就尝试其它密钥,最后全部失败后报Permission denied。
排查方式很简单:
ssh -v 别名 # 注意输出中这行 debug1: identity file /path/to/deploy_key type -1 # type -1 表示文件不存在或类型无法识别看到type -1,大概率就是路径写错或者文件权限不对。另外,如果~/.ssh/config里的User写错了,也会导致认证失败,因为 SSH 在密码认证时会尝试使用配置文件中的用户名登录远程服务器,与真实用户名不一致就必然失败。
提示:alos 值得注意的配置项是
ServerAliveInterval 60。如果通过 SSH 连上服务器后长时间不操作会被断开,这类问题虽然不直接表现为Permission denied,但和连接稳定性密切相关。尤其在使用 VSCode Remote-SSH、Tabby 这类需要长连接的 SSH 工具时,设置这个参数能有效避免“写着写着代码连接断了”的恶心体验。
3.6 算法不兼容与老版本 SSH
还有一类Permission denied既不涉及密钥内容,也不涉及权限,而是加密算法的兼容性问题。新版本 OpenSSH 默认禁用了旧版的ssh-dss(DSA) 算法,如果你的服务器和客户端有一方是老古董,就会遇到认证方式全部被拒但日志里看不出明显错误的情况。
曾经接了一个内部系统迁移的活,对方的一台老物理机 SSH 版本停在 5.3(CentOS 6.10 时代),而客户端用的是新版 OpenSSH。连接时服务端根本不提供客户端期望的算法,密码认证和公钥认证双双失败。后来只能用-o参数临时指定兼容算法才连上:
ssh -oHostKeyAlgorithms=+ssh-dss 用户名@服务器IP客户端如果太老,连不上新服务端理也常见。新版 OpenSSH 默认禁用了 RSA/SHA1 签名,老客户端只支持 SHA1,于是认证失败。解决方案是在服务端或者客户端临时启用,具体取决于谁老谁新:
# 服务端 /etc/ssh/sshd_config 中开启旧算法兼容(视版本而定) PubkeyAcceptedAlgorithms +ssh-rsa处理这类问题时不要追求彻底解决,最稳妥的方案是把操作系统的 SSH 升级到现代版本,不建议长期开启低安全强度的旧算法。
3.7 服务端 StrictModes 开启与文件属主错误
StrictModes这个参数默认是yes,它在公钥认证阶段会严格检查.ssh、authorized_keys以及家目录的属主和权限。如果authorized_keys的属主不是当前登录用户,即使权限正确也会直接被拒。
典型场景是:管理员用 root 帮普通用户把公钥写进了 authorized_keys,然后没有执行chown,文件属主还是 root。用户登录时 SSH 检查发现问题,直接拒绝:
Authentication refused: bad ownership or modes for file /home/deploy/.ssh/authorized_keys修复方式也很简单:
chown -R deploy:deploy /home/deploy/.ssh还有一个取巧的做法,就是把StrictModes改成no绕过检查。但这不是正规方案,等于把 SSH 的自我保护机制关掉,强烈不建议在生产环境这么干。你永远不知道哪天会因为某个文件属主错误而丢了一台服务器。
3.8 VSCode 远程连接特有的问题:重复密钥选择与资源占用
VSCode 的 Remote-SSH 本质上是调用了命令行 SSH 连接工具,因此所有之前提到的Permission denied原因都会原封不动地影响到它。但 VSCode 远程连接还有自己的一层额外逻辑:它在连接成功后会往远程服务器下载插件和vscode-server,这一步如果失败,你可能会看到类似于 "connecting to ssh host ..." 的弹窗一直卡住。
这类情况有时会被用户误认为是认证被拒,其实真正的报错隐藏在 VSCode 输出面板的日志里。一个经常出现的场景是:本地 SSH 配置文件~/.ssh/config中配置了多个 Host,Remote-SSH 插件在连接时可能会选中错误的 Host,导致用户名、端口全错,然后报出Permission denied (publickey)。
实操心得:排查 VSCode 远程连接问题时,我建议先打开输出面板并切换到 “Remote-SSH” 频道,看清里面的 debug 日志。查看日志时对照前文提到的
-v输出,能把 90% 的问题定位出来。
4. 实战排障流程:从日志到修通的完整路径
前文讲了很多理论上的原因,这一节把整个流程串起来,给出一套可以直接照着做的排障动作。
4.1 用好 Debug 模式,让 SSH 自己开口说
SSH 的调试参数分为-v、-vv、-vvv三档,数字越大输出越啰嗦。日常排查Permission denied,我一般直接用-vvv,信息全,不怕多看:
ssh -vvv 用户名@服务器IP这时候客户端会输出几十行调试信息,重点关注几个关键标志:
debug1: Connecting to ... port 22:说明正在建立 TCP 连接,如果卡在这里,就是网络层问题。debug1: Connection established.:TCP 已经连通。debug1: Authentications that can continue: publickey,password:服务器允许哪些认证方式。debug1: Next authentication method: publickey:客户端正在尝试公钥。debug1: Trying private key: /home/xxx/.ssh/id_rsa:客户端在用哪个私钥尝试。debug1: Authentications that can continue: publickey,password:认证失败后服务器再次列出可用的认证方式。
这几行输出能帮你快速定位问题在哪一层。我曾经只靠ssh -vvv的输出就判断出同事的问题出在服务器端authorized_keys属主错误,因为命令行的输出从头到尾连“尝试私钥”的日志都没有直接成功断行,一切都在服务器端被拒绝。
4.2 服务端日志,比客户端信息更精确
客户端的信息再详细,也看不到服务端拒绝原因。真正的权威在服务端日志里。
Debian/Ubuntu 系看这个:
tail -f /var/log/auth.logCentOS/RHEL 系看这个:
tail -f /var/log/secure如果日志中出现Failed password for invalid user,说明服务端上根本没有这个用户名,权限问题在用户名本身。如果出现Failed password for deploy from ...,说明用户名存在但密码或公钥不正确。如果出现Authentication refused: bad ownership or modes,恭喜,权限问题被服务端准确抓到了,按第三条里的权限修复走一遍即可。
再举个例子,有一次我排查服务器频繁被爆破的问题,服务器在/var/log/secure里记录了大量Failed password for root from ...的日志,其中有一台客户机器则是反复出现认证失败,最后排查发现是一个已离职同事的密钥没有被回收,但他的用户已经被删了,所以我们怎么看都找不到被拒的原因。把无效用户和无效密钥清理掉之后,问题消失了。
4.3 标准排查顺序,尽量减少盲目操作
根据实际排障的流程,这里给出一套可复制的标准顺序:
- 确认网络连通性和端口状态:
nc -vz -w 5 服务器IP 22 - 确认用户名正确:
whoami,如果服务器上是 root 还要检查PermitRootLogin - 尝试密码认证:手动输入正确密码,确认服务端允许密码认证
- 查看服务端日志,确认拒绝类型
- 检查客户端
-vvv输出,弄清当前用的认证方式和私钥 - 检查两端文件权限:家目录、
.ssh、authorized_keys - 检查
~/.ssh/config的配置:HostName、Port、User、IdentityFile - 清空 ssh-agent 缓存后手动指定密钥测试
- 检查服务端
sshd_config修改后是否重载、是否有语法错误 - 最后检查 SELinux、防火墙和 hosts.allow/deny
这套顺序看起来简单,但能覆盖 95% 的Permission denied问题。我见过很多人跳过了前两步,直接从改密钥权限开始,结果改了半小时发现连端口都没通,纯属浪费时间。
重要提示:不要把
ssh这个命令当成黑盒。它就是一条双向协商的协议,任何一步协商失败都会有输出,只要学会看输出,排障效率直接翻倍。
5. 常见问题速查表与避坑技巧
为了让后续排查更加顺手,我整理了一张速查表,把前面提到的所有问题压缩成一行一行,方便实际问题发生时快速查对。
5.1 高频问题速查表
| 报错现象 | 可能原因 | 快速处理 |
|---|---|---|
Permission denied, please try again | 用户名或密码错误 | 重新确认凭据,查看服务端日志 |
Permission denied (publickey) | 密钥未被服务端识别 | 检查 authorized_keys 内容和权限 |
Permission denied (publickey,password) | 用户名错或认证方式受限 | 确认用户存在,检查 sshd_config 认证参数 |
Host key verification failed | known_hosts 指纹不匹配 | ssh-keygen -R 服务器IP然后重连 |
| 连接直接超时 | 防火墙拦截或 sshd 未监听 | nc -vz检查端口连通性 |
| VSCode 反复要求输入密码 | 远程 server 下载失败或身份文件未加载 | 查看 Remote-SSH 日志,清理服务器 vscode-server |
| 密码正确但立刻退出 | MaxAuthTries太小 | 增大MaxAuthTries,或避免客户端先尝试错误密钥 |
| git push/pull 报权限问题 | git remote 地址里用户名或端口错误 | 检查git remote -v,使用 SSH config 别名 |
使用密钥登录但报错文件中type -1 | IdentityFile 路径错误或文件不存在 | 检查~/.ssh/config中的路径 |
| SELinux 开启导致公钥认证失败 | ssh 相关布尔值未开启 | setsebool -P ssh_sysadm_login on |
5.2 提升 SSH 连接稳定性的建议
Permission denied只是 SSH 连接问题的第一道坎,跨过之后还有各种稳定性和安全性问题。既然标题提到了远程服务器连接,这里就多说一点下面的建议,都是被现实教育过的经验:
优先使用密钥认证而不是密码认证。密钥比密码长得多、难以猜测,而且可以单独为客户端的私钥设置 passphrase。密码认证在暴力破解工具面前就是一层薄纸,尤其是把 22 端口暴露在公网上的服务器,一天可以收到成千上万次连接尝试。如果你自己的服务器 ip 只是临时暴露,至少把端口改成非标准值并启用 fail2ban。
关于 fail2ban,这是一个极其实用的防护工具。它能消费/var/log/secure或/var/log/auth.log的日志,自动识别多次认证失败并临时封禁来源 IP,能极大减少暴力破解的影响。配合前文的日志分析,你会发现 fail2ban 的过滤规则和日志字段天然对应。
经常使用 SSH 跳过局域网或者跨地区连接服务器时,可以考虑借助内网穿透工具。拿搜索热词里的 Tailscale 为例,这个工具可以把局域网穿透到异地设备,实现了远程直连的效果,这让本来需要公网 IP 才能连的服务器,变得像局域网一样顺畅。当然,这类工具本身对 SSH 来说只是隧道层面的事情,连接后Permission denied的错误依然和普通场景一样。
如果是和团队协作,为每个成员分配独立账号和独立密钥是必须的。谁负责哪台服务器、谁改过配置,排查的时候一清二楚。运维里面最害怕的不是故障,而是“不知道是谁动过”的故障。账号隔离之后权限问题边界会非常清晰。
最后是日志。提前把/var/log/auth.log或/var/log/secure接入集中日志系统,或者用简单的 logrotate + 远程同步方式归档,问题出现时直接翻日志,不然临时上去tail -f也有自己的成本。
5.3 一次完整的实战复盘
为了让大家对排查顺序有更直观的感受,这里还原一个真实的排障过程。
那时候有个项目需要在一台新服务器上部署服务,本地已经生成了 ed25519 密钥。我把公钥加到服务器的authorized_keys之后,执行连接命令:
ssh deploy@192.168.1.50结果提示Permission denied (publickey)。按照前面的排查顺序,我先看了nc -vz -w 5 192.168.1.50 22,端口正常。接着看了眼服务端日志:
tail -5 /var/log/auth.log输出:
Failed password for deploy from 192.168.1.20 port 52210 ssh2这说明服务端撑到了密码认证,而且没有触发公钥认证,或者公钥认证走完就失败了。我再用ssh -vvv连续输出,看到:
debug1: Offering public key: /home/local_user/.ssh/id_ed25519 ED25519 SHA256:xxxx debug1: Authentications that can continue: publickey,password这说明客户端已经把公钥提供过去了,但服务端不接受。于是到服务端检查authorized_keys,发现文件属主是 root,权限是 644。按照前文的规范修改成deploy:deploy和 600 之后,再连接就直接成功了。
这个案例中没有任何“玄学”,每一步都有日志支撑有问题,定位其实很快。最怕的是凭感觉去试,一会儿改密码,一会儿重启 sshd,结果什么都没有解决。
6. 一些长期有用的习惯
文章写到后半程,补充几个和排查不一定直接相关、但对运维日常有实际帮助的习惯。
6.1 用代理命令(ProxyJump)管理多主机跳转
实际生产环境中,很多服务器不直接对外开放,需要通过跳板机中转。这种情况下,SSH 的ProxyJump选项非常省事:
ssh -J jump_user@跳板机IP target_user@目标服务器IP你一旦掌握这个技巧,维护一批内网机器时就不用反复登录跳板机再 ssh 到目标机器了。配合~/.ssh/config可以定义所有目标主机的跳转路径:
Host jump HostName 跳板机IP User jump_user Host internal-server HostName 192.168.1.100 User deploy ProxyJump jump这样直接执行ssh internal-server即可。
6.2 别忽略了 keepalive 和连接超时
当你使用 VSCode、Tabby 这类远程开发工具时,SSH 连接经常因为长时间的空闲被服务端或中间设备断开。这时除了前文提到的ServerAliveInterval,还可以在服务端 sshd_config 里设置:
ClientAliveInterval 60 ClientAliveCountMax 3这个参数让服务端每隔 60 秒向客户端发送一次存活检测包,一共探测 3 次,之后仍然没有回应就断开连接。有了这个设置,只要客户端机器网络正常、SSH 程序还在,长会话基本不会无故中断。
6.3 密钥的备份与轮换
很多人对生成 SSH 密钥这件事不够重视,觉得丢了就重新生成。丢了个私钥可能意味着所有服务器都要重新配置公钥,有更严谨的做法是定期轮换密钥,并在安全的地方备份一份加密的私钥副本。我自己习惯用密码管理器管理服务器清单,并在其中保存私钥的加密版本,以便团队内部协同。
6.4 小心对待未知来源的密钥
处理服务器时,如果发现authorized_keys里多了一行不认识的内容,不要轻视。这往往意味着服务器可能曾被未授权访问过,或者某个环节存在漏洞。处理方式是立即移除未知公钥、修改 root 密码、检查sshd_config是否有被篡改的地方,并查看登录日志确认是否存在异常登录。这类安全问题比Permission denied本身严重得多,需要尽快处理。
用 Tailscale 这类工具把服务器纳入内网后,大量原本暴露在公网的服务可以直接关闭对外端口,这让 SSHD 的暴露面大大缩小。我在自己的基础设施里就是这么做的:公网完全不开 SSH,只通过内网工具访问,暴力破解日志从每天几千条直接变成零。这个思路值得大家在规划远程访问时考虑。
换句话说,Permission denied很多时候只是问题的表面,往深处挖,往往能发现网络架构、用户权限、密钥管理等方面还有不少可以优化的地方。解决一个报错的同时,顺手提升一下整体配置,可能比单纯把问题堵住更有价值。
以前我遇到问题总习惯第一时间找工具,后来发现自己冷静下来逐层排查,反而更快。SSH 本身就提供了完整的诊断手段,只要用好-vvv和服务端日志,大部分Permission denied都能在十分钟内定位。希望这篇内容能帮你从这类报错中走出来,也让 SSH 变成你手里真正可靠的工具。