1. SSH配置这件事,为什么值得单独写一整篇
1.1 先说说SSH在服务器日常里的位置
如果你是从前面几篇一路"修炼"过来的,应该已经体会到了:服务器这东西,买来之后就是一个黑盒子,你在命令行里敲下的每一条指令,绝大多数都要经过SSH这条通道。不管你是CentOS、Ubuntu还是Rocky Linux,只要你是通过命令行操作远端服务器,SSH就是你日常接触最多的协议,没有之一。
这一篇之所以叫"配置和保护SSH",是因为我发现很多人在部署环境的时候,装完Nginx、MySQL、中间件,却常常忽略掉自己脚下这根最关键的管道。你在服务器上跑得再花哨的服务,本质都是先把SSH这条入口守住。配置SSH,解决的是"怎么连得顺手"的问题;保护SSH,解决的是"怎么不被外部爆破和打穿"的问题。二者缺一不可。
1.2 默认状态下的SSH,暴露面比你想象中大
新装的服务器,如果你不去动任何配置,默认是22端口、密码认证、root可登录。很多人觉得"我的密码够复杂就行",但你可以试想一下:一台拥有公网IP的机器,从开机那一刻起,就会有来自全世界的扫描流量在试探它,日志里那些不断尝试登录的IP就是证据。默认的密码登录方式,本质上就是把一个写满用户名的门牌号贴在了公网上——root是一个确定的用户名,SSH端口是确定的22,剩下的就是对你的密码进行穷举。
这就像你在一个鱼龙混杂的小区里买了一套房,交付时开发商给你配了一把普通的A级锁,虽然能用,但左邻右舍都知道你家大门是这种锁。而配置SSH密钥认证和调整登录限制,就是把门锁换成指纹锁。这不是危言耸听,而是每一个长期维护服务器的人都会经历的阶段。
注意:在做任何SSH加固之前,请先确认自己有控制台VNC或救援模式作为最后兜底手段。云服务商的控制台一般都提供网页版VNC或终端入口,这是你改崩溃了sshd配置之后唯一的救命稻草。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从密码到密钥:认证方式的核心区别
2.1 密码认证为什么不够用
密码认证的工作方式很简单:客户端把用户名和密码提交给服务端,服务端去验证。问题在于,密码本身是"可猜测的",而且它可以通过无数次的尝试来暴力破解。哪怕你设置了密码错误延迟,也只能降低爆破速度,不能根除爆破行为。更麻烦的是,一套稍微复杂的密码,在多个环境之间容易重复使用,一旦某个网站的数据库泄露,攻击者会拿着这组用户名密码去批量尝试你的服务器。
只要你的服务器还开放着密码认证,你的授权文件、密钥、账号体系就暴露在持续的攻击面之下。即便业务上需要第三方临时访问,正规做法也是生成一次性的密钥,而不是把密码发给对方。
2.2 密钥认证的原理,一句话就能讲明白
密钥认证用的是非对称加密。简单理解就是:服务器上存放一把"锁"(公钥),你的电脑上保管一把"钥匙"(私钥)。当你想登录时,服务器会验证你确实持有那把跟公钥配对的私钥。整个过程不需要在网络中传输密码,私钥也永远不出现在服务器上。
这里有个生活化类比:你去健身房办卡,办了之后前台电脑里记录了你的指纹特征(公钥),你每次进门按一下指纹(私钥的持有证明),系统验证通过就放行。密码相当于你每次进门都报一次身份证号,而公钥/私钥跟你本人是绑定的。
2.3 为什么推荐优先使用Ed25519算法
生成密钥的时候,主流的算法有RSA和Ed25519两种。RSA是过去的通用标准,密钥长度至少要3072位或4096位才算安全。Ed25519是更新的椭圆曲线签名算法,密钥长度短、速度快、安全性高。
我在项目实践中全部使用Ed25519来生成密钥,生成的公钥以ssh-ed25519开头。老的Windows客户端或某些特殊环境如果兼容性差,再用RSA 4096兜底。以2026年的使用环境来说,绝大多数SSH客户端都支持Ed25519,不必为兼容性担心。
3. 常规加固第一轮:先改四个关键参数,把门锁上
3.1 打开sshd_config前的准备工作
SSH服务端的配置文件,不同发行版有细微差别。常见路径是/etc/ssh/sshd_config。改配置之前建议先把原文件留个备份:
bash复制sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
这条命令会生成带日期的备份文件,后面万一改坏了可以随时回滚。修改配置的工具用vim、nano都可以。改完配置之后有一个固定动作:先检查语法,再重启服务,顺序不能反。
bash复制sudo sshd -t
sudo systemctl restart sshd
sshd -t只会做语法检查,不会重启服务,如果写错了配置,它会直接给出哪一行有问题的提示,而不是让你在断线之后才意识到服务起不来。
3.2 禁用root直接登录:PermitRootLogin no
对于绝大多数场景,root用户直接通过SSH登录都不应该被允许。原因很朴素:root是每台Linux机器上都存在的固定用户名,攻击者不需要猜用户名,只要猜密码。禁用root直接登录,同时保留普通用户登录后再切换root的能力,是成本最低的账户暴破缓解措施。
涉及的具体操作是两步。第一步,先创建一个有sudo权限的普通用户:
bash复制sudo useradd -m -G wheel admin
# 或者 Debian/Ubuntu 系列的写法:
sudo useradd -m -G sudo admin
sudo passwd admin
这里-G wheel是把用户加入wheel组,前提是系统里已经存在wheel组。CentOS/RHEL系默认有wheel组,sudo的配置里默认允许wheel组使用sudo;Ubuntu/Debian系默认用sudo组,没有wheel。然后编辑/etc/ssh/sshd_config,找到下面这行:
code复制PermitRootLogin no
实操中还有一个坑:某些云主机镜像是把PermitRootLogin yes写在单独的/etc/ssh/sshd_config.d/目录里的,主配置里看不到。改之前建议先执行:
bash复制sudo grep -r "PermitRootLogin" /etc/ssh/
把主配置和sshd_config.d/下的所有文件都查一遍,防止有某个配置文件在最后把参数覆盖回去。
3.3 只允许wheel组的用户可以登录:AllowGroups wheel
只禁用root还不够,理想状态是"谁能通过SSH登录"由你说了算,而不是任何在系统里有账号的人都能登录。把SSH登录权限限制在一个组里,是最清晰的管理方式。
在sshd_config里加入这一行:
code复制AllowGroups wheel
如果你的系统是sudo组而不是wheel组,就写成AllowGroups sudo。这个配置的意思是:只有属于wheel组的用户能够通过SSH登入系统,其他用户即使账号存在、密码正确,也会在认证阶段直接被拒绝。这也正好回应用了热词里出现过的"设置只有wheel组的用户可以SSH远程登录"——碰到这个需求就是一句话的事。
我把用户加入wheel组用的是usermod -aG wheel 用户名,注意-a参数不能省,否则会把用户从其他组里清出来。
3.4 允许的用户白名单:AllowUsers更精确的控制
如果你管理的用户比较多,需要精确到用户级别,则可以在sshd_config中使用:
code复制AllowUsers admin deploy
这样设置后,除了admin和deploy,其他任何用户都无法SSH登录。尤其适合多账号共存一台机器的部署场景。
3.5 修改默认端口与监听地址
把SSH默认端口从22改成高位端口,比如22822,是我一直推荐的做法。虽然从安全的角度来说,靠改端口隐藏服务,稍微懂行的人扫一下就能发现,但它能挡住绝大多数"无差别扫描"的流量。那些批量扫描脚本一般只会扫常见的22端口,改成高位端口之后,日志里的噪声会骤降。
修改方法:
code复制Port 22822
ListenAddress 0.0.0.0
如果只想让内网IP访问SSH,监听地址可以写成ListenAddress 192.168.1.10,效果是只有内网机器能连,外网直接被拒绝。修改端口前的关键提醒是:改完Port之后,防火墙和云安全组必须同步放行新端口,否则重启SSH的一瞬间,你与服务器的连接就会断开且新连接也进不来。
这是我实测踩过的坑中最常见的一个。云服务器都有两层防火墙:第一层在云控制台的安全组,第二层是服务器里的firewalld、ufw或iptables。两层都得放行新端口之后,才能去重启sshd。
3.6 登录超时和连接频率限制
sshd_config里还有几个容易被忽略的参数,但对提升安全性有明显效果:
LoginGraceTime 30:登录超时时间(秒),超过这个时间用户没有认证成功就断开连接。MaxAuthTries 3:单次连接最多只能尝试3次认证。ClientAliveInterval 300:服务端每300秒向客户端发一个保活包,如果客户端断开,服务端能及时发现并释放连接。ClientAliveCountMax 2:连续2个保活包没响应就断开。
不用把它调得太激进,一个普通运维人员如果登录时指纹按慢了就不会被卡。但LoginGraceTime 30和MaxAuthTries 3组合起来,能大幅拉低每个来源IP对SSH的暴力破解效率。
4. 真正的密钥登录配置:从生成到分发一次打通
4.1 在本地生成密钥对
我在本地使用SSH客户端生成密钥对,以Linux/macOS终端为例:
bash复制ssh-keygen -t ed25519 -C "your_email_or_nickname" -f ~/.ssh/cloud_key
参数解释一下:-t指定算法类型,-C是一个备注信息,方便你以后知道这把公钥是哪台机器、用来做什么的,-f指定私钥保存路径。执行过程中,系统会提示你设置passphrase。生产环境强烈建议设置,平时自己学习用的环境可以不设,但要清楚不设的风险是私钥文件一旦泄露,别人就能直接登录你的服务器。
4.2 把公钥安装到服务器上
服务器端需要把公钥写进对应用户的~/.ssh/authorized_keys文件里。最常见的方式是用ssh-copy-id:
bash复制ssh-copy-id -i ~/.ssh/cloud_key.pub -p 22 admin@服务器IP
如果端口是修改后的,需要把-p换成新端口。这个命令会要求你输入一次密码,验证通过后自动把公钥追加到服务器的authorized_keys文件里,并顺手把权限设置好。
没有ssh-copy-id的Windows环境,就手动操作:先打印公钥内容:
bash复制cat ~/.ssh/cloud_key.pub
然后SSH登录服务器,把内容追加进去:
bash复制mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "这里粘贴公钥内容" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700和chmod 600这两条权限设置不是走过场,如果权限太开放,sshd会直接拒绝使用这个authorized_keys文件来认证。很多"密钥配置了却还是提示要密码"的情况,八成就是权限这里出了问题。
4.3 用私钥登录并取消密码认证
公钥放好后,先不要急着关掉密码认证,而是先验证密钥登录有没有生效。在本地执行:
bash复制ssh -i ~/.ssh/cloud_key -p 22822 admin@服务器IP
能直接登录进去之后,再去修改/etc/ssh/sshd_config:
code复制PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
这里的PasswordAuthentication no是关闭密码登录的开关。ChallengeResponseAuthentication也是跟密码验证相关的,Debian系系统尤其要一起关掉,否则即使PasswordAuthentication设为no,PAM那一层依然可能允许密码登录。UsePAM no这条有争议,很多系统依赖PAM做额外的认证,关闭之前先确认没有其他需要PAM的模块。我的习惯是先保持UsePAM yes,主要关掉前两个参数,实测足够严格。如果追求极限,就执行完上面所有行后立即重开一个SSH窗口测试,不要关旧的连接窗口。
4.4 SSH config:把连接参数固化下来,告别一长串命令
既然都配到了这一步,本地连接就直接用~/.ssh/config文件管理,让日常操作更顺心。在本地文件里增加一段:
code复制Host dev-server
HostName 123.45.67.8
Port 22822
User admin
IdentityFile ~/.ssh/cloud_key
保存之后,本地只需要输入:
bash复制ssh dev-server
就能直接登录。这段配置把你的服务器IP、端口、用户名、私钥路径全部固化到Host别名里,非常推荐。
5. 远程操作安全自救:先想好退路再动手
5.1 永远保留至少一条应急通道
我刚入门的时候,有一次就是急着加固,在线上机器里直接修改sshd_config,把Port改成了高位端口且忘了修改云安全组,保存退出后重启sshd,旧的连接断开了,新的连接进不去,整个人当场红温。后来去云控制台通过VNC登录,才把配置改回来。
从那以后我给自己定下一条规矩:在做任何SSH配置变更之前,先开启一个新的SSH会话或在控制台开一个VNC窗口。VNC会话不依赖sshd,所以哪怕sshd服务彻底挂了,你依然能进入系统救急。
操作顺序要固定:先备份配置,再修改,然后sshd -t检查语法,最后再重启sshd。重启后不要马上关掉手头的连接窗口,要再新开一个窗口测试连接,新窗口能正常登录,才算这个变更安全落地。
5.2 从密码认证切换到密钥认证的过渡方案
如果你是第一次在生产机器上关闭密码认证,最稳妥的办法是分两步走。第一天上公钥、测试密钥登录;第二天再把PasswordAuthentication改为no并重启sshd。中间留出一天时间,万一你的某个设备没有保存密钥,还有机会补救。
这个节奏对于一个人维护多台服务器的情况尤其重要。我自己遇到过把所有服务器的密码认证都关掉了,结果某天换了台新笔记本,忘了把私钥拷贝过去,导致要用那台新电脑访问服务器时发现所有旧方式都被禁了。最后靠手机的终端App里的旧私钥才连上。教训是:迁移密钥的优先级要排在换设备之后的第一位。
6. 常见故障对照表:密钥登录的翻车现场与修复办法
下面把我在Git Bash、Windows命令行、VSCode、Bitvise SSH Client等各类环境下踩过或帮助读者排查过的高频问题列出来。
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
| SSH连接提示Connection refused | sshd未启动、端口不对、防火墙拦截 | 用VNC登录排查sshd状态,检查端口监听与云安全组 |
| 提示Permission denied (publickey,password) | 密码认证已关闭、公钥没装对、权限错误 | 检查authorized_keys内容,调整~/.ssh目录权限为700、文件权限为600 |
| 服务器日志提示Offending key或REMOTE HOST IDENTIFICATION HAS CHANGED | 服务器重装系统后host key变化、known_hosts有旧记录 | 用ssh-keygen -R 服务器IP清除旧记录后重新连接 |
| 密钥文件权限太开放 | 私钥文件有group读权限,SSH直接拒绝使用 | chmod 600 ~/.ssh/私钥文件 |
| ssh-agent 没有加载私钥 | 重启系统或更换终端后agent丢失 | ssh-add ~/.ssh/cloud_key 或配置 ~/.ssh/config 中加 AddKeysToAgent yes |
| Bitvise SSH Client登录时提示认证方式不匹配 | 服务端只允许publickey,客户端还在用password模式 | 新建Profile时选用publickey认证并指定私钥文件 |
运维排查时最忌讳一堆设置乱猜。由于服务器端的日志会给出最直接的判断依据,建议先用root或sudo权限查看日志:
bash复制sudo journalctl -u sshd -n 50
# 或者 CentOS/RHEL 系:
sudo tail -f /var/log/secure
在日志里找到类似Failed publickey for admin from 1.2.3.4或Connection closed by authenticating user这样的条目,就知道到底哪一步没通过。
这里提一个VSCode远程开发的场景:VSCode连接SSH远程服务器的原理就是Remote-SSH扩展调用本地SSH客户端。如果本地的~/.ssh/config里已经配置能用密钥登录,VSCode一般也能直接连上。如果VSCode提示认证失败,先回到终端里试一次ssh dev-server,终端能连通而VSCode连不上,多半是ssh-agent没把密钥加载到VSCode继承的环境变量里。解决方案是在~/.ssh/config里对目标主机加一行:
code复制IdentityFile ~/.ssh/cloud_key
AddKeysToAgent yes
然后重启VSCode窗口,问题基本能解决。
7. 周边防护不可少:防火墙、防爆破与操作审计
7.1 防火墙精确放行:不要裸奔公网
一个平时会被忽略的习惯是:很多人的安全组和服务器防火墙规则都太随意,要么是全开,要么是3306、6379这类端口直接暴露给公网。SSH加固做完了,建议顺便把防火墙收敛一下。
在CentOS/RHEL系上通常用firewalld操作:
bash复制sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --permanent --add-port=22822/tcp
sudo firewall-cmd --reload
Debian/Ubuntu系如果是ufw:
bash复制sudo ufw allow 22822/tcp
sudo ufw deny 22/tcp
sudo ufw enable
操作完成后用ss -lntp | grep 22822确认sshd确实监听在新端口,再用本地终端测试连接。这个过程对于新购服务器来说一般5分钟就能完成,但能省的麻烦远不止5分钟。
7.2 fail2ban:给暴力破解加一道刹车
仅仅关闭密码登录还不够彻底,毕竟服务器可能还有邮件服务、Web管理后台等其他暴露的入口。如果开启了密码认证(比如内网某些旧设备的场景),强烈建议装一个fail2ban来做暴力破解的自动封禁。
安装也很简单:
bash复制sudo yum install -y epel-release fail2ban
# Debian/Ubuntu:
sudo apt install -y fail2ban
在/etc/fail2ban/jail.local里配置:
ini复制[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/secure
maxretry = 3
bantime = 3600
maxretry表示失败3次就封IP,bantime是封禁的秒数。如果改过了端口,这里port要写成对应的端口数字。启动fail2ban之后,拿另一个IP试几次错误密码,然后fail2ban-client status sshd就能查看到封禁结果。
7.3 日志与审计:什么时候、谁登进来过
安全防护的最后一块拼图是"事后能看到"。养成关注登录日志的习惯,能帮助你及时发现异常。常用的排查命令:
bash复制last -n 20
journalctl -u sshd | grep "Accepted"
last -n 20显示最近的登录记录,包括登录者用户名、来源IP、时间。如果发现某个时间段有大量来自陌生IP的登录尝试,那就要检查一下是否有账号被爆破成功了。另一个实用的做法是配置邮箱或IM通知,每次root或管理员用户登录就发一条提醒。方案有很多,但都不复杂,关键是去做了。
8. 实战收尾:我个人的一套高性价比SSH加固流程
下面这个流程是我在新服务器上稳定执行的系列动作,按这个顺序走完,SSH基本不会出太大问题:
- 创建管理员用户,加入wheel组,设置强密码。
- 本地生成Ed25519密钥,用
ssh-copy-id把公钥装上去。 - 用密钥登录验证通过后,修改
/etc/ssh/sshd_config。 - 把
PermitRootLogin改为no,加入AllowGroups wheel,加入PasswordAuthentication no、ChallengeResponseAuthentication no,把端口改成高位端口。 - 执行
sshd -t检查语法,然后systemctl restart sshd。 - 在保持当前连接不断开的情况下,新开一个SSH窗口测试新配置。
- 在云控制台安全组和服务器防火墙里,只放行新SSH端口,关闭旧端口。
- 安装fail2ban,把ssh服务的爆破次数限制住。
- 把本地的连接参数固化到
~/.ssh/config,减少以后输入错误。
根据我这些年维护服务器的经验,很多人配置SSH最后出问题,不是参数写错了,而是操作节奏没控制好。正确做法是每次只改少量参数并验证通过后再继续下一项,不要一次性把所有加固都改完然后期待万无一失。你面对的毕竟是一台远程机器,任何改动都要考虑自己当前这个连接是不是还活着。
还有个小技巧分享给你:把sshd_config里需要调整的行统一整理到一个自定义文件,比如在配置文件末尾添加:
code复制Include /etc/ssh/sshd_config.d/*.conf
然后在/etc/ssh/sshd_config.d/下新建一个security.conf,把上述加固项写进去。这样升级OpenSSH或重装系统时,你的加固策略还能原样保留,不会因为并行安装包覆盖主配置而丢失。
SSH配置和保护这件事,说难不难,说简单也不简单。它不像是部署一个中间件那样能给你带来立竿见影的功能成就感,但它决定了你之后每一次运维是否安心。当你把日志里的陌生扫描降到几乎为零,当你用一条ssh dev-server就能无障碍进入自己的机器时,你才能明白前期这几分钟的加固工作有多值。修仙路上没有一蹴而就,但今天你把门锁好了,后面飞升的时候就不用回头补墙了。
