☰
SSH远程连接Permission denied排查思路与实战指南
2026/10/1 14:47:56 网站建设 项目流程

深夜往服务器上部署代码,屏幕冷冰冰地弹出一行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,几个直接影响认证结果的参数需要重点关注:

参数取值含义
PasswordAuthenticationyes/no是否允许密码认证
PubkeyAuthenticationyes/no是否允许公钥认证
PermitRootLoginyes/prohibit-password/no是否允许 root 登录
MaxAuthTries数字单次连接允许的最大认证尝试次数
PermitEmptyPasswordsyes/no是否允许空密码登录
AuthorizedKeysFile文件路径指定公钥存放位置
StrictModesyes/no是否检查用户文件和目录权限

这些参数看起来简单,但排列组合起来能产生很多让人摸不着头脑的问题。曾经帮人排查一台服务器,密码明明是对的,但一连接就被踢,-v调试输出显示服务端反复要求重新认证,最后发现问题出在MaxAuthTries设置为 2,而他本地的 SSH 客户端先尝试了公钥认证失败又继续尝试密码认证,两次失败之后直接被踢出,根本轮不到第三次输入正确密码。

修改完 sshd_config 之后一定要重启服务,而且要先重载再重启,避免语法错误把服务搞挂:

# 先检查配置语法 sshd -t # 再优雅重启 systemctl reload sshd

sshd -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_key

3.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.log

CentOS/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 标准排查顺序,尽量减少盲目操作

根据实际排障的流程,这里给出一套可复制的标准顺序:

  1. 确认网络连通性和端口状态:nc -vz -w 5 服务器IP 22
  2. 确认用户名正确:whoami,如果服务器上是 root 还要检查PermitRootLogin
  3. 尝试密码认证:手动输入正确密码,确认服务端允许密码认证
  4. 查看服务端日志,确认拒绝类型
  5. 检查客户端-vvv输出,弄清当前用的认证方式和私钥
  6. 检查两端文件权限:家目录、.ssh、authorized_keys
  7. 检查~/.ssh/config的配置:HostName、Port、User、IdentityFile
  8. 清空 ssh-agent 缓存后手动指定密钥测试
  9. 检查服务端sshd_config修改后是否重载、是否有语法错误
  10. 最后检查 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 failedknown_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 -1IdentityFile 路径错误或文件不存在检查~/.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 变成你手里真正可靠的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询