真正接管一台服务器的时候,几乎没人会去动那些默认的sshd_config——只要root密码能登录,SSH也好使,运维们就不愿折腾。直到某一天你用Git推送、VSCode远程开发,或者要批量管理几百台机器的密钥,才发现默认的SSH登录配置根本扛不住,而很多坑一开始就埋在那份只有几百行的/etc/ssh/sshd_config里。
SSH服务配置详解这句话,说到底是围绕sshd_config这份文件展开的。它决定了一台机器允许谁登录、用什么方式登录、登录之后能做什么、连接挂了怎么处理,几乎你能想到的SSH安全问题,都能在这里找到对应的开关。你会发现网上搜“ssh密钥”“ssh免密码登录配置”“vscode连接ssh远程服务器”“设置只有wheel组的用户可以ssh远程登录”,最终大家操作的全是这份文件里的几个关键指令。这篇内容不打算面面俱到地把man手册抄一遍,而是把真正容易被搞混、生产环境里一定会用到的指令拿出来逐个拆解,并附上我实际踩过的坑。
1. 为什么SSH配置值得单独写一篇:你面对的不是一个端口,而是一整套认证策略
很多人对SSH的理解停留在“能连上就行”。一台机器装好系统,默认开着22端口,SSH服务就能用了,你甚至不会去问它背后发生了什么。但SSH服务通常是一台服务器对外暴露的第一个入口,尤其对云主机、物理机、办公网络里的Linux服务器来说,只要IP可路由,全世界都在扫描22端口。
我见过太多服务器开着root密码登录,然后被扫描器反复尝试爆破。这些尝试大部分来自固定脚本,密码足够强可能一时半会儿攻不进来,但日志每天几百条Failed password,看着就心慌。更要命的是,一旦某个账户的密码在其他平台被拖库泄露,撞库扫描几分钟就能把尝试打到你这台机器上。
sshd_config的价值恰恰在于:它让你从被动挨打变成主动设防。你可以关掉密码认证、禁止root直接登录、把访问范围缩小到指定用户或指定组、甚至把SSH端口改到非标准位置。这些操作不需要额外装任何安全软件,不需要改防火墙,只需要在配置里写清楚策略,然后重载服务。
另一个维度是工程效率。开发环境里总离不开Git推送、VSCode远程开发、批量部署脚本,而“免密登录”依靠的其实是密钥认证加上ssh-agent转发等机制。你跟这些工具之间的连接通道是否稳定,本质上也取决于sshd_config里几个关键参数。比如很多人改了端口之后发现VSCode连不上,再一看是配置文件里Port和实际监听的端口不一致;比如有人把PasswordAuthentication设成no,结果忘了先把公钥放进去,把自己锁在外面。这些都属于“配置没理清,生产环境火葬场”的典型场景。
所以这篇的定位不是教你背参数,而是帮你建立一份关于sshd_config的操作地图:每个指令影响什么、改完怎么验证、出错怎么回滚。下面我们从最基础的语法开始。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. sshd_config 基础语法与加载机制:注释、键值对、语法检查和重载
2.1 配置文件位置和基本格式
在绝大多数Linux发行版上,SSH服务端配置文件路径是/etc/ssh/sshd_config。注意,它和客户端配置文件/etc/ssh/ssh_config是两个完全不同的东西。前者配置的是sshd服务端,也就是“谁连进来”的规则;后者配置的是ssh客户端,也就是“你发起连接时的默认参数”。很多人搜“SSH配置文件详解”搜出一堆内容却觉得对不上,多半是混淆了这两者。
sshd_config的语法非常规整:每一行要么是参数配置,要么是注释,要么是空白行。
code复制# 这是一行注释
Port 22
Protocol 2
PermitRootLogin prohibit-password
格式基本都是“参数名 参数值”,参数名不区分大小写,但惯例上使用首字母大写的驼峰形式。配置文件里以#开头的行是注释,很多指令在默认配置里会以注释形式展示默认值,去掉注释就相当于显式声明用这个默认值。这里有个容易踩的小坑:有时候你看到默认配置里注释掉了一行#PermitRootLogin prohibit-password,以为没生效,但实际上prohibit-password就是很多发行版编译时的默认值,所以即使不放开注释,root也不能用密码登录。对这种“注释即默认”的理解偏差,会导致排查方向完全跑偏。
2.2 配置的加载、重载与检查
SSH服务并不是每次连接都去重新读配置,而是在启动时加载一次。所以修改完sshd_config后,必须让服务重载配置。不同系统的服务名不太一样:
- 使用systemd的发行版:
systemctl reload sshd,有些系统叫ssh,比如Debian/Ubuntu默认服务名是ssh,RHEL系叫sshd。 - 传统SysVinit:
service sshd reload或者直接kill -HUP $(cat /var/run/sshd.pid)。
生产环境里,我强烈建议任何修改之后先用语法检查命令过一遍:
code复制sshd -t
这条命令会解析配置文件并报告语法错误。如果输出为空,说明语法通过。如果写错了某个参数名、漏了参数值、或者Match块的语法不对,它会明确提示第几行有问题,此时绝对不要重启sshd服务,否则可能导致现有SSH连接断掉,甚至新连接也无法建立。
还有一个容易被忽略的命令是sshd -T,它会把当前生效的配置完整输出出来,包括那些你没写但使用了默认值的参数。调试的时候我特别喜欢用sshd -T去确认某个指令的实际运行值,比如排查到底PermitRootLogin当前是yes还是prohibit-password,直接执行:
code复制sshd -T | grep -i permitrootlogin
这样看到的就是服务端真正运行时的值,比翻注释文件和手册更靠谱。
2.3 同一参数多处出现和Match块的生效规则
配置文件里同一个参数如果出现多次,默认情况下以第一个出现的值为准,不是最后一个。这一点和其它很多配置文件的习惯相反,容易让人产生错觉。比如你想在文件顶部写Port 22,后来又在文件末尾写了Port 2222,实际上生效的是先出现的22,除非你用Match或include机制去改变加载顺序。
另一个现代sshd_config里很重要的结构是Match块。它用来对特定用户、用户组、源地址等条件应用不同配置。
code复制Match User root
PasswordAuthentication no
Match Address 192.168.1.*
PermitRootLogin yes
注意,Match块必须以Match条件开头,块内所有配置只对匹配的会话生效。如果某个条件没有匹配,但你在里面配置了参数,这个参数就会被忽略,不会影响其他连接。实际运维中,我经常用它给特定管理网段放开root登录,而对外网保持禁用,这种细颗粒度的控制比写死全局参数要好用得多。
3. 登录认证的真实开关:PasswordAuthentication、PubkeyAuthentication 与 PermitRootLogin 三兄弟
网上搜“ssh免密码登录配置”“如何取消使root用户也可以ssh登录”这类问题时,你最终操作的基本都是下面三个核心认证开关。
3.1 PasswordAuthentication:密码登录的总闸门
这个参数控制是否允许使用密码进行认证。默认值通常是yes,但凡是稍微做过安全加固的服务器,基本都会改成no,改成no之后,登录时唯一可用的认证方式就是公钥认证(或者其他PAM支持的认证方式)。
如果把PasswordAuthentication no理解成“禁止所有密码登录”,这个认知方向是对的,但有一个特殊情况要留意:当系统里还存在其他认证模块,例如LDAP、OTP、双因子认证等,密码可能通过PAM栈被验证,此时PasswordAuthentication no会影响到多少,取决于sshd如何调用PAM。在生产环境里,如果你开了双因子认证或企业统一认证,贸然把PasswordAuthentication改成no,极有可能把相关认证流程也一起禁用。
正确的做法是,先检查你的认证栈。如果你只是想让普通用户使用密钥登录,那PasswordAuthentication no没有问题。如果你要保留密码输入的入口,但又想禁止root用密码登录,那需要配合PermitRootLogin来控制,而不是一刀切关密码认证。
3.2 PubkeyAuthentication:让密钥登录变得可用
公钥认证是SSH里最常用的免密登录基础。它对应参数是PubkeyAuthentication yes,绝大多数发行版默认开启。这里真正值得深挖的是公钥从哪来、怎么存放、权限怎么检查。
服务端默认读取的是每个用户家目录下的~/.ssh/authorized_keys文件,文件路径由AuthorizedKeysFile参数指定。这个文件里的每一行都是一个公钥,对应一个允许登录该用户的密钥。免密登录的原理就是客户端持有私钥,服务端保存公钥,登录时通过签名挑战完成身份认证。
有个常见的误解是“我把公钥放进去就能免密了”。实际上,许多发行版还受StrictModes影响,如果~/.ssh目录或authorized_keys文件权限太宽松,比如组用户可写,sshd会出于安全考虑直接拒绝这个公钥。具体表现是:连接时看不到密码提示,但输入密码后会断开,日志里写Authentication refused: bad ownership or modes。
所以配置免密登录时,务必要把目录和文件权限收缩到位:
code复制chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R $(whoami):$(whoami) ~/.ssh
这个细节是“ssh免密码登录配置”里最容易翻车的地方,然而网上很多教程只贴命令不解释权限问题,以至于新手反复删配置也搞不定。
3.3 PermitRootLogin:root用户的登录策略
PermitRootLogin可能是整个配置文件里最敏感、最容易被搜索的参数。它主要有这几个值:
| 参数值 | 含义 | 适用场景 |
|---|---|---|
| yes | 允许root使用任何认证方式登录 | 仅建议用于完全隔离的内网/虚拟机 |
| prohibit-password | 允许root使用密钥登录,禁止密码登录 | 需要root直连但又要防爆破的机器 |
| no | 禁止root直接登录 | 使用普通用户登录后su/sudo切换 |
| forced-commands-only | 仅允许执行特定命令,且只能使用密钥认证 | 自动化备份、远程执行受限命令 |
如果你搜过“如何取消使root用户也可以ssh登录”,这里要分两种理解:一种是彻底禁止root登录,设PermitRootLogin no;另一种是取消root的密码登录方式,改为仅允许密钥登录,那就设PermitRootLogin prohibit-password。这两种方式对应完全不同的安全需求,别只看字面意思就随便改。
我个人的建议是:凡是可以使用普通用户加sudo的运维场景,直接把它设为no,让所有行为都带上用户审计;只有在自动化脚本必须使用root权限、并且你已经做好密钥管理和来源限制时,才考虑prohibit-password。至于yes,哪怕在内网也不推荐,因为内网一旦被横向穿透,root弱口令就变成提款机。
3.4 改配置前必须做好的逃生预案
把PasswordAuthentication改成no、把PermitRootLogin改成no,都是那种改完就可能让自己进不去的危险操作。我每次操作远程服务器前,会强制自己先开一个备用SSH会话,确认备用会话在修改配置之后依然能新建连接,再动配置。如果只有一个会话,那改之前必须准备好通过云控制台VNC、物理终端或者带外管理卡登录的渠道。
因为一旦你在配置里把密码认证关了,但公钥还没放进去,或者放进去的公钥由于权限问题被拒绝,唯一的结果就是你把自己锁在自己服务器外面。这类事故在论坛里天天发生,原因基本都是缺少逃生通道。正确顺序是:
- 先把本地公钥追加到服务器的
~/.ssh/authorized_keys里。 - 用另一台机器或另一个终端测试密钥登录是否成功。
- 确认无误后再把
PasswordAuthentication改为no,并执行语法检查和重载。 - 再新开一个连接验证,确认还能通过密钥登录。
这个流程看着啰嗦,却是唯一不会把自己关在外面的操作顺序。
4. 通过 AllowUsers、AllowGroups 和 DenyGroups 把 SSH 的使用者范围锁死
你搜“设置只有wheel组的用户可以ssh远程登录”,本质上就是想知道怎么让SSH只接受某类用户。sshd_config里对应的机制是AllowGroups、AllowUsers、DenyGroups、DenyUsers这四个前缀指令。它们的判断逻辑不算复杂,但细节上有些坑。
4.1 先理解默认放行逻辑
默认情况下,配置里没有AllowUsers或AllowGroups,那么系统上所有合法用户都可以尝试SSH登录,只要认证通过就能进来。想限制范围,有三种常见做法:
- 只允许指定用户:
AllowUsers alice bob - 只允许指定组:
AllowGroups sshusers - 同时组合:
AllowUsers root@192.168.1.* alice
重点是,只要存在Allow开头的指令,就意味着默认拒绝所有未匹配的请求。这里容易被误解的是AllowUsers和AllowGroups同时存在时的行为。如果你写:
code复制AllowUsers alice
AllowGroups sshusers
ssh会要求用户必须同时满足两个条件吗?并不是。官方文档对allow/deny的处理顺序是:先DenyUsers,再AllowUsers,然后DenyGroups,最后AllowGroups。四条指令分别判断,只要有任意Allow匹配就算放行,并不是说既要在AllowUsers里又要在AllowGroups里。让我用一个例子展开:
假设服务器上有用户alice属于组sshusers,用户bob属于组developers,配置如下:
code复制AllowUsers alice
AllowGroups sshusers
alice既在AllowUsers里又在sshusers组里,当然能登录。bob不在AllowUsers里,但AllowUsers已经存在,bob本来应该被拒绝,可因为AllowGroups匹配的是sshusers,而bob不在sshusers组里,所以还是被拒绝。如果bob在sshusers组里呢?此时他能登录,因为AllowGroups匹配到了他。换句话说,AllowUsers和AllowGroups是“或”的关系,不是“与”的关系。
想做到“必须同时在两个条件里”,就只能用Match User加Match Group嵌套条件。但在实际需求里,通常只需要一个维度就够了。
4.2 只允许wheel组用户远程登录的落地配置
在RHEL/CentOS系服务器上,wheel组就是管理员组,普通用户的sudo权限通常通过wheel组来赋予。设置“只允许wheel组用户SSH登录”,配置文件里写:
code复制AllowGroups wheel
如果希望某个用户不在wheel组但也要能登录,可以追加:
code复制AllowUsers deployer
这里的组合逻辑刚才已经解释过,deployer即使不属于wheel组也能登录。很多新手在这种配置下会困惑:“我已经写了AllowGroups wheel,为什么deployer还能进来?”原因就是AllowUsers与AllowGroups是“或”的逻辑。
如果希望保持严格唯一,即只允许wheel组,不要用AllowUsers额外开口子。如果确实需要额外用户,更清晰的方式是把这个用户加入wheel组,然后用一条AllowGroups wheel统一管理:
code复制usermod -aG wheel deployer
这样整台服务器的SSH访问边界就非常清晰:能SSH进来的,一定在wheel组里。普通员工账号即使创建了,也无法远程登录,需要先以管理员身份登录,再用su或sudo切换。
4.3 Match块实现按来源地址隔离
有时候你不想全局限制用户范围,而是希望内网IP的访问策略和公网访问策略不一样。比如内网管理员可以用密码登录,而公网只允许特定密钥登录,可以用Match实现:
code复制Match Address 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
PasswordAuthentication yes
PermitRootLogin yes
Match Address *
PasswordAuthentication no
PermitRootLogin no
这里要注意Match必须写在配置文件末尾,因为一旦进入Match模式,后面所有配置都会当作Match块内容处理,直到文件结束。如果你在Match块之后再写一条普通配置,它会被当作Match内的下一层配置去解析,大概率会报错。
5. 密钥与其他认证协同:批量登录和自动化场景下的AuthorizedKeysFile策略
热门词里的“ssh批量登录”“git ssh配置教程”“vscode连接ssh远程服务器”,最终都会落到“把哪些公钥放进哪台服务器的哪个位置”。如果机器多,手动追加公钥显然不现实,需要理解AuthorizedKeysFile的扩展能力。
5.1 默认公钥文件位置与自定义AuthorizedKeysFile
默认情况下,sshd读取~/.ssh/authorized_keys。你可以通过以下配置改变路径:
code复制AuthorizedKeysFile .ssh/authorized_keys
注意这个路径如果是相对路径,是相对于用户家目录。如果你设置了绝对路径,比如/etc/ssh/authorized_keys/%u,那么所有用户的公钥都集中在系统目录下,好处是统一管理、方便自动化批量下发,坏处是普通用户无法在权限内自行维护自己的公钥,而且这个目录的权限必须严格控制为root。
在大量服务器需要批量登录的运维场景里,我常用的一种方案是:制定一台跳板机或管理节点,管理员在管理机上生成一对密钥,然后把公钥推送到几十上百台服务器的同一运维用户下。管理用户固定为ops,家目录下放authorized_keys,里面可以叠多行公钥,一个公钥一行。这样一来,运维人员登录跳板机后就能免密访问多台服务器,省去反复输密码的成本。
5.2 批量下发密钥的常见方式
手工一台台追加肯定不高效。常见做法是用自动化工具,比如Ansible。如果你没有配置好Ansible的免密通道,那么可以先用ssh-copy-id实现单台推送:
code复制ssh-copy-id -i ~/.ssh/id_ed25519.pub ops@192.168.1.101
ssh-copy-id会自动把公钥追加到远程用户家目录的~/.ssh/authorized_keys中,并帮忙修正目录权限,是简化免密配置的神器。首次连接需要输入远程用户的密码。
当要下发到几十台机器时,最稳妥的方式是把Ansible的authorized_key模块用起来:
code复制- name: 配置运维用户公钥
authorized_key:
user: ops
state: present
key: "{{ lookup('file', '/home/admin/.ssh/id_ed25519.pub') }}"
只要Ansible控制机能通过一台初始的账号连上所有机器,那么公钥下发就是幂等的,重复执行不会在authorized_keys里产生重复行。
5.3 多密钥与ssh-agent:一台客户端连多个服务端
开发类人群更关心的场景是:我这台电脑上有公司GitLab用的专属密钥,也有个人GitHub用的密钥,还有好几台服务器分别绑定了不同的密钥。ssh客户端默认会尝试~/.ssh/id_rsa、~/.ssh/id_ed25519等默认私钥,如果服务端不认识,就会继续尝试密码认证。
解决方案是为不同域名或主机指定不同密钥。在客户端配置文件~/.ssh/config里写:
code复制Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
IdentitiesOnly yes
Host myserver
HostName 192.168.1.10
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519_server
这里的IdentitiesOnly yes非常重要,它告诉ssh客户端只使用你明确指定的IdentityFile,而不会把所有私钥都拿去试探。如果你经常遇到“用GitHub时提示服务器拒绝我们的密钥”或者密钥顺序错误,多半是这个问题。
5.4 服务端日志对排查密钥问题至关重要
配置了密钥,连接还是失败时,不要盯着客户端空转,去服务端或者客户端带上-vvv参数看详细输出,重点看服务端日志。在systemd系统上执行:
code复制journalctl -u sshd -f
然后客户端重试连接,日志中会出现类似Connection closed by authenticating user admin这样一目了然的信息。引起这个问题的原因非常多:authorized_keys权限不对、用户不存在、公钥格式不对、AllowUsers里没写这个用户等。读日志始终是最接近真相的办法,比在网上反复搜报错信息要高效得多。
6. 连接超时、心跳保持和端口冲突:ClientAliveInterval、Port与监听地址的选择
很多人搜“centos ssh time”“ssh连接很快就断”“vscode连接ssh远程服务器一段时间就断开”,其实都指向了一组跟会话保活相关的参数。这些细节平时不起眼,但一旦出现,体验感会非常糟糕。
6.1 Port与ListenAddress:把服务监听到你想要的位置
默认Port 22意味着sshd监听在所有网卡的22端口上,无论外网还是内网都能连。如果你的服务器只有单一网络用途,也许没问题;但如果一台机器既在公网又在多个内网网段,你可能会希望让SSH只监听特定IP。
code复制Port 2222
ListenAddress 0.0.0.0
比如上面配置会让sshd在所有IPv4地址的2222端口监听。如果你只希望监听公司办公网段地址和内网地址而不监听公网,就可以这么写:
code复制Port 2222
ListenAddress 10.0.0.10
ListenAddress 172.16.0.10
但要注意,改了端口之后,所有客户端连接命令都需要显式加-p 2222,防火墙规则、云安全组规则也要同步放行。很多人在调试“ssh: connect to host github.com port 22: connection refused”这类问题时,就会想到是不是自己服务器端口被防火墙限制了,通常在云主机上,麻烦多半出在云安全组而非本机防火墙。
在真正改端口之前,你要先想清楚一件事:是出于安全防护目的,还是纯粹需要避开某个端口占用冲突?如果是前者,要明白改端口只能挡掉一部分惯用扫描器的盲扫,并不能替代密钥认证和访问控制,真正有效的安全策略是关闭密码登录配合AllowUsers。如果是后者,直接把Port改成新端口即可,没什么可犹豫的。
6.2 ClientAliveInterval和ClientAliveCountMax:避免连接被防火墙杀掉
服务器端和客户端之间默认有一个连接保活探测机制。有些用户离开电脑一会儿,回来发现终端已经断掉,原因可能是办公网防火墙、NAT网关或云安全策略会在长时间无数据流量后杀掉这条空闲连接。
sshd服务端有两个参数可以设置服务端主动探测客户端是否存活:
code复制ClientAliveInterval 60
ClientAliveCountMax 3
ClientAliveInterval 60表示如果服务端60秒内没有收到客户端任何消息,就主动发送一个加密的存活请求。ClientAliveCountMax 3表示连续发送3次都没有收到响应后,服务端才断开连接。也就是超过3分钟无响应才断开。这个配置对常见的NAT超时掉线有很大改善,尤其是在使用VSCode Remote-SSH或长时间挂着tmux会话时非常有帮助。
对应的客户端侧配置在/etc/ssh/ssh_config或用户级~/.ssh/config里,通常配合设置:
code复制Host *
ServerAliveInterval 30
ServerAliveCountMax 3
简单说,客户端主动发心跳比服务端发探测在某些网络环境下更可靠。但如果你没有权限改客户端配置,仅在服务端开ClientAliveInterval也能起到保持连接活跃的效果。
6.3 MaxSessions与MaxStartups:别让资源被连接池占死
如果服务器承载的SSH访问量比较大,比如跳板机要被几十个运维同时使用,那么MaxSessions和MaxStartups这两个参数就会发挥作用。
MaxSessions控制每个SSH连接上可以打开的最大会话数,默认是10。一次SSH连接后面其实可以承载SCP、SFTP、端口转发等多个会话通道,如果你总是遇到“打开多个终端标签页就卡死”,检查一下这个参数。在自动化批量执行命令时,如果并发太高,也可以调大一些。
MaxStartups则控制最大并发连接数。它的默认配置形如10:30:100,表示并发连接数达到10之前无限制;超过10开始随机拒绝部分新连接,拒绝概率逐步升高;到100之后全部拒绝。对普通服务器来说默认值够用,但对暴露在公网的强密码认证服务器,一旦被大量扫描连接打满,也会导致正常运维登录很困难。对这种情况,与其反复调这个参数,不如尽早把密码认证关掉、把访问范围通过AllowUsers缩小,源头上减少无效连接。
7. 实操排错:从几次“SSH连不上”到定位sshd_config问题的通用链路
热词里大量出现“vscode连接ssh远程服务器提示失败”“ssh连接windows认证失败”“GitHub端口22 connection refused”这类问题。虽然根源千差万别,但排查时最好按照从下往上的顺序,一层层剥开。
7.1 第一步:确认服务端进程本身在不在
先确认SSH服务有没有正常运行。
code复制systemctl status sshd
如果显示Active: active (running),说明进程是活的。如果显示failed,再看下日志:
code复制journalctl -u sshd --no-pager | tail -50
很多时候sshd启动失败是因为配置文件语法错误,比如某一行参数拼写错了,或者Match块位置不对。此时用sshd -t找出具体错误行号,先修正语法再重启。
服务端正常时,下一步测试网络和端口通不通。本地可以用telnet或nc测:
code复制nc -vz 目标IP 22
如果端口不通,再看防火墙。CentOS上常用firewall-cmd --list-all,Ubuntu上用ufw status。更前一步,如果目标在云上,还要去云安全组里确认入方向是否放行对应端口和来源IP。
7.2 第二步:验证认证阶段到底卡在哪里
网络端口没问题,但登录还是失败,就要打开客户端的调试输出看看卡在哪个环节:
code复制ssh -vvv admin@目标IP
-vvv输出会包含很多行,重点看几次报错:
- 出现
Permission denied (publickey,password),说明认证没通过。如果是密码登录失败,去服务端看日志;如果是密钥登录失败,重点看公钥是否已追加、权限是否正确、AllowUsers是否包含该用户。 - 出现
connection closed by remote host,多半是服务端直接把连接关了,此时服务端日志里一般会有明确原因,比如User admin from 1.2.3.4 not allowed because not listed in AllowUsers。 - 出现
Connection reset by peer,可能是端口不对、防火墙拒绝了连接,或者服务根本不在这个端口上。 - 出现
kex_exchange_identification相关错误,说明TCP建立成功但SSH协议握手没完成,通常因为服务端处理连接超限,或源IP被某个防护工具拉黑。
很多人连不上的时候习惯盲目重装SSH服务,其实99%的问题通过合理配置都能解决,不必走到重装那一步。
7.3 第三步:配置改完没生效时,用sshd -T做对照实验
我经常碰到这样的反馈:“我改了Port 2200,也reload了,为什么连接还是显示端口22拒绝?”这种时候,先别急着怀疑reload没执行,而要看配置文件里是不是存在多个Port行,或者被include的其它文件覆盖。
执行:
code复制sshd -T | grep -E '^(port|listenaddress)'
如果输出的port还是22,说明你修改的那一行并没有成为最终生效的第一优先级配置。此时去全文搜索一下Port出现的位置:
code复制grep -n -i '^Port' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf
如果/etc/ssh/sshd_config.d/下面还有别的conf文件,而主配置文件末尾带有Include /etc/ssh/sshd_config.d/*.conf,那部分发行版会在主配置之前或之后追加加载这些文件。不同发行版处理include的顺序略有不同,但通常都是把目录中的配置当作一个合并文件处理,如果目录里有Port设置,也会成为竞争项。此时最好的做法是统一在一个地方管理端口,例如就在主配置文件里写,其它残留的Port行全部注释掉。
7.4 第四步:借助deny和allow指令复盘登录失败原因
当你觉得配置没问题但某个用户就是登录不进来时,直接读服务端日志往往最快定位。不过还有一种情况是,日志里没有明确写为什么拒绝,只写了Connection closed by authenticating user xxx preauth。
这时你需要回到配置本身的逻辑。比如前面说的AllowUsers和AllowGroups是“或”关系,你如果既写了AllowUsers限制某用户A,又写了AllowGroups限制某组B,而A并不在B组里,那就得想清楚你是否真的希望A能登录。如果是“必须只允许某组里的用户登录”,就不要写AllowUsers。配置里的指令本来就不多,仔细捋一遍能发现八成都是范围放大了或缩小了。
7.5 一些通用的验证手段
为了减少排查时的黑盒感,还可以使用ssh -G 目标别名查看客户端实际解析后的连接参数:
code复制ssh -G myserver | grep -E 'port|identityfile|user'
这个命令非常有用,尤其当你的~/.ssh/config里有大量别名、ProxyJump、各种IdentityFile时,它能直接告诉你客户端最终会用哪个端口、哪个用户、哪个私钥去连,省得拿-vvv一层层翻耗时费力。
如果只是在服务端想确认某个用户当前有哪些公钥被接受,可以执行:
code复制sshd -T | grep authorizedkeysfile
然后按路径去检查文件内容。
8. 几种典型场景下的sshd_config推荐配置
看完了各参数的机制,最后给出几个相对常用且经过验证的基础配置模板,方便你结合业务情况调整。这些配置不是标准答案,只是给一个“已经被跑过很多次”的参考。
8.1 简单生产服务器安全基线
适合面向公网的轻度业务服务器,这台机器只运行Web服务或API,不需要太多人登录。
code复制Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowGroups wheel
ClientAliveInterval 60
ClientAliveCountMax 3
X11Forwarding no
这套配置直接关闭了root密码登录、关闭密码登录,只允许wheel组用户通过密钥进入。由于wheel组用户本身就带sudo权限,管理员登录后用sudo完全能覆盖日常管理工作。
X11Forwarding对绝大多数服务器都没有必要,关掉能减少转发类攻击面。如果你要在服务器上跑图形应用并通过SSH转发,例如用X11跑某个管理界面,再单独开启。
8.2 Git托管及自动化部署服务器
如果服务器承担的是Git仓库、CI/CD构建节点、批量部署等自动化任务,那这些任务通常通过专用机器账号跑,要允许远程程序用密钥拉取或推送代码,但又不希望这个账号能登录交互式Shell。
配置的亮点在于Match User git块的forced-commands-only,它可以结合command=前缀限制公钥能执行的命令。
code复制Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Match User git
PasswordAuthentication no
AllowTCPForwarding no
X11Forwarding no
PermitTTY no
ForceCommand git-shell
ForceCommand git-shell会把git用户的所有SSH命令强制限制在git-shell里,即使有公钥,也无法获得一个正常Shell。当然,如果只是想让自动化任务通过SSH执行固定脚本,也可以写成ForceCommand /opt/bin/deploy-helper。
8.3 VSCode Remote-SSH开发环境
开发者用VSCode连接远程服务器编写代码时,一个顺畅的配置除了认证方式之外,还应该保证SFTP子系统在内的子系统都正常工作。VSCode Remote-SSH本质上依赖远程主机上的sftp子系统来管理文件传输,如果配置里手滑把Subsystem禁用,VSCode会显示连接到了SSH但一直没法完成初始化。
code复制Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers dev
Subsystem sftp /usr/lib/openssh/sftp-server
ClientAliveInterval 60
ClientAliveCountMax 3
有些环境为了安全会把sftp替换成internal-sftp并配合ChrootDirectory做监狱目录。但如果你的目标是VSCode Remote-SSH,就别随手加Chroot监狱,除非你能确定远程打开的目录都在允许范围内,否则会导致编辑器只能看到空目录。
还有一点,VSCode在连接远程主机时会自动维护远端~/.vscode-server目录,如果该目录权限不对或者磁盘空间满了,也会引起奇怪的连接失败。这类问题和sshd_config没有直接关系,但也值得纳入排错视野。
8.4 跳板机/堡垒机
跳板机通常只需要一个入口账号,并为这台机器背后的生产网段打开端口转发或ProxyJump。
code复制Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowGroups bastion
AllowAgentForwarding yes
AllowTcpForwarding yes
GatewayPorts no
ClientAliveInterval 120
ClientAliveCountMax 3
客户端可以利用这个跳板机做ProxyJump,配置在~/.ssh/config里:
code复制Host target-server
HostName 10.20.30.40
User appuser
ProxyJump bastion
注意,如果跳板机的AllowTCPForwarding被设成no,那么即使客户端配置了ProxyJump,连接依然会失败。跳板机上只做端口转发的话,GatewayPorts保持默认的no也足够用。
9. 最后的经验:文件备份、变更记录和回归验证比任何高级参数都重要
如果你已经看到了这里,大概率不是只想看参数名,而是真的准备去改自己的服务器配置文件。那我必须提醒一个在实际中不知救了多少人的习惯:任何修改sshd_config前,先备份。
备份不是简单复制一个文件,而是具备回滚能力的副本:
code复制cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d%H%M%S)
为什么强烈建议这样做?因为sshd_config是所有参数的大杂烩,你改的时候可能只改一行,但当你在生产服务器上排查问题时,这一行和原有配置的隐性组合可能产生无法预料的后果。如果有备份,遇到问题直接恢复原始文件再reload就行,不用靠记忆反推自己改过什么。
修改完配置文件后,别忘了执行:
code复制sshd -t && systemctl reload sshd
用&&连接,只有语法检查通过才会真正执行重载。如果语法检查失败,systemctl reload sshd不会执行,不会产生任何配置变更。
最后,保留一个当前会话不关闭,再另开一个会话验证新配置生效。这是至今为止最有效的回归验证方式。整个SSH登录过程本身就包含网络、客户端、服务端、认证、文件权限等链路,能把这些链路用的时候走通一遍,以后遇到再复杂的连接问题,也能保持判断力。
我把这些年在sshd_config上踩过的坑基本都写在这里了。你不需要一次性把上面所有参数全部启用,关键是先梳理清楚自己服务器的使用人群和登录方式,再逐项对照配置文件判断该开哪个关哪个。每次只改一小块,验证一小块,SSH这种基础服务才能既安全又稳定。
