拿到一台新的云服务器后,不要立刻关闭 root 登录或修改防火墙。正确的顺序是:先确认当前会话和登录来源,再创建普通管理员账户并验证密钥登录,最后启用 SSH 加固、Fail2Ban 和防火墙规则。
本文主要适用于使用 systemd 的现代 Linux 发行版。命令以 RHEL、Rocky Linux、AlmaLinux、Fedora 的 firewalld 为主,同时给出 Debian、Ubuntu 的对应操作。
在云厂商控制台中保留一个可用的串行终端、VNC 或救援模式。修改 SSH 和防火墙时始终保留当前会话,并用另一个终端测试新配置;确认新会话可以登录后,再退出旧会话。
1. 先检查谁正在登录
安全加固前,首先确认服务器上是否存在不认识的会话和来源 IP。
1.1 查看当前登录用户和会话
# 当前登录用户、终端、登录时间、空闲时间和来源地址
who -uH
# 当前用户及其正在运行的命令
w
# systemd 管理的登录会话
loginctl list-sessions
# 查看某个会话的详细信息,将 SESSION_ID 替换为上一条命令中的 ID
loginctl session-status SESSION_ID
who 和 w 读取登录会话记录,适合回答”目前有哪些用户登录”;loginctl 还能显示会话类型、本地或远程来源以及相关进程。它们可能包含同一用户的多个终端,因此不要只看用户名,还要核对来源地址和登录时间。
如果只想检查已经建立的 SSH TCP 连接,可以执行:
sudo ss -tnp state established '( sport = :22 )'
如果 SSH 不使用 22 端口,需要替换端口号。ss 展示的是网络连接,和 who 的会话记录互为补充;文件传输、端口转发等 SSH 连接不一定表现为交互式登录会话。
当前 SSH 会话的客户端 IP 位于 $SSH_CONNECTION 的第一列:
printf '%s\n' "$SSH_CONNECTION"
ADMIN_IP=$(printf '%s\n' "$SSH_CONNECTION" | awk '{print $1}')
printf '当前管理端 IP: %s\n' "$ADMIN_IP"
后面手工封禁 IP 时,先核对目标地址不等于这个管理端 IP。
1.2 查看近期成功和失败的登录
# 近期成功登录和重启记录;-a 将来源地址放到最后一列,-i 保留数字 IP
last -ai | head -n 30
# 近期失败登录,需要 root 权限;部分系统没有启用 btmp 记录
sudo lastb -ai | head -n 30
# 从 systemd journal 查看当天的 sshd 日志
sudo journalctl _COMM=sshd --since today
# 只筛选常见的成功、失败和无效用户事件
sudo journalctl _COMM=sshd --since today | grep -E 'Accepted|Failed|Invalid user'
发现陌生的成功登录时,不要只封禁一个 IP。还应立即检查 ~/.ssh/authorized_keys、sudoers、计划任务、systemd 服务和异常进程,并轮换可能泄露的密钥或密码。
2. 创建普通管理员账户
直接使用 root 远程登录有两个明显问题:账户名固定、权限没有缓冲。更稳妥的做法是创建一个普通账户,仅在需要时通过 sudo 提权。
2.1 创建账户并授予 sudo 权限
RHEL、Rocky Linux、AlmaLinux、Fedora:
sudo useradd --create-home --shell /bin/bash userA
sudo passwd userA
sudo usermod -aG wheel userA
Debian、Ubuntu:
sudo adduser userA
sudo usermod -aG sudo userA
新开一个终端,用 userA 登录并验证:
ssh userA@SERVER_IP
sudo -v
sudo id
输出中出现 uid=0(root) 才说明 sudo 配置有效。在验证完成前,不要关闭当前 root 会话。
如果需要单独配置 sudoers,使用 visudo 检查语法,不要直接编辑 /etc/sudoers:
sudo visudo -f /etc/sudoers.d/userA
允许完整 sudo 权限的语法是:
userA ALL=(ALL:ALL) ALL
免密 sudo 会让窃取到普通账户或其 SSH 密钥的攻击者更容易直接取得 root 权限,因此不建议全局设置 NOPASSWD: ALL。自动化任务确实需要免密时,只授权固定的、不能被用户替换或间接执行任意命令的程序。
3. 配置 SSH 密钥登录
3.1 在客户端生成密钥
新的配置优先使用 Ed25519,并为私钥设置口令:
ssh-keygen -t ed25519 -a 100 -C "userA@client"
默认会生成 ~/.ssh/id_ed25519 私钥和 ~/.ssh/id_ed25519.pub 公钥。私钥不能上传到服务器,也不能通过聊天或邮件发送;建议将私钥口令交给密码管理器保存。
3.2 把公钥安装到服务器
最简单的方法是在客户端执行:
ssh-copy-id -i ~/.ssh/id_ed25519.pub userA@SERVER_IP
如果客户端没有 ssh-copy-id,可以先把公钥复制到服务器,然后以 userA 身份执行:
install -d -m 700 ~/.ssh
cat ~/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
rm ~/id_ed25519.pub
这里必须使用 >> 追加公钥;使用 > 会覆盖已有公钥。目录权限应为 700,authorized_keys 应为 600。
在客户端验证密钥登录:
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 userA@SERVER_IP
失败时增加 -vvv 查看客户端调试日志,同时在服务器查看 SSH 日志:
ssh -vvv -i ~/.ssh/id_ed25519 userA@SERVER_IP
sudo journalctl _COMM=sshd -n 100 --no-pager
3.3 配置客户端别名
编辑客户端的 ~/.ssh/config:
Host cloud-server
HostName SERVER_IP
User userA
Port 22
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
以后使用 ssh cloud-server 即可登录。只有明确需要代理转发时才启用 ForwardAgent,不要把它作为默认设置。
4. 加固 SSH 服务
确认普通账户的密钥登录已经成功后,再修改 /etc/ssh/sshd_config,或使用发行版已经启用的 /etc/ssh/sshd_config.d/*.conf:
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 30
如果使用 AllowUsers,要把所有需要远程登录的账户列全,否则未列出的账户会被拒绝:
AllowUsers userA
应用配置前必须先检查语法:
sudo sshd -t
命令没有输出才表示语法检查通过。然后重新加载服务:
# RHEL、Rocky Linux、AlmaLinux、Fedora
sudo systemctl reload sshd
# Debian、Ubuntu 通常使用 ssh.service
sudo systemctl reload ssh
不要立即退出当前会话。新开终端确认普通账户仍能登录,再使用下面的命令检查最终生效值:
sudo sshd -T | grep -E 'permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|maxauthtries|logingracetime'
4.1 可选:启用真正的 2FA
2FA 应要求两种不同因素,而不是”公钥或验证码”二选一。通过 PAM 配好 TOTP 后,可以要求客户端依次通过公钥和交互式验证码:
UsePAM yes
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive:pam
AuthenticationMethods 中逗号表示必须依次完成多个验证步骤,空格才表示不同的可选组合。PAM 模块的安装、恢复码保存和紧急账户策略因发行版不同,启用前应保留控制台入口,并先为每一个允许登录的用户完成 TOTP 注册。

5. 用防火墙临时封禁登录来源
手工封禁适合正在发生的攻击或应急处置;持续的暴力破解应交给下一节的 Fail2Ban。执行前先通过 who、w、ss 和 $SSH_CONNECTION 核对 IP,避免封掉自己的管理地址。
5.1 firewalld:设置自动过期时间
先确认网卡所在的活动 zone:
sudo firewall-cmd --get-active-zones
下面示例在 public zone 中封禁 IPv4 地址 203.0.113.10 对 SSH 22 端口的访问,并在 1 小时后自动删除规则:
BLOCK_IP=203.0.113.10
SSH_PORT=22
ZONE=public
RULE="rule family=\"ipv4\" source address=\"$BLOCK_IP\" port port=\"$SSH_PORT\" protocol=\"tcp\" reject"
sudo firewall-cmd --zone="$ZONE" --add-rich-rule="$RULE" --timeout=1h
sudo firewall-cmd --zone="$ZONE" --query-rich-rule="$RULE"
sudo firewall-cmd --zone="$ZONE" --list-rich-rules
--timeout 接受秒数以及 s、m、h 后缀。这个规则只存在于运行时配置,过期后自动移除,不能和 --permanent 一起使用。需要提前解封时,在同一个 shell 中执行:
sudo firewall-cmd --zone="$ZONE" --remove-rich-rule="$RULE"
IPv6 地址需要把规则中的 family="ipv4" 改成 family="ipv6"。如果服务器修改过 SSH 端口,也要同步修改 SSH_PORT。规则语法和超时行为可参考 firewall-cmd 官方手册及 firewalld rich rule 说明文档。
5.2 UFW:手工新增和删除临时规则
UFW 的常规命令没有规则 TTL,可以先插入规则,处置结束后用完全相同的条件删除:
sudo ufw insert 1 deny from 203.0.113.10 to any port 22 proto tcp
sudo ufw status numbered
# 解除封禁
sudo ufw delete deny from 203.0.113.10 to any port 22 proto tcp
不要同时手工维护 firewalld、UFW、iptables 和 nftables 多套规则。先用 systemctl is-active firewalld、ufw status 判断当前服务器实际由谁管理防火墙。
6. 使用 Fail2Ban 自动封禁 SSH 暴力破解
Fail2Ban 持续读取认证日志,在一个时间窗口内失败次数超过阈值后调用防火墙封禁来源 IP,并在 bantime 到期后自动解封。它能减少日志噪声和暴力破解,但不能替代密钥认证、及时更新系统和最小权限。
6.1 安装 Fail2Ban
Fedora,或已经启用 EPEL 的 RHEL 兼容发行版:
sudo dnf install fail2ban
如果 Rocky Linux、AlmaLinux 等系统提示找不到软件包,需要先按对应系统版本启用 EPEL,再安装 Fail2Ban。Debian、Ubuntu:
sudo apt update
sudo apt install fail2ban
6.2 配置 sshd jail
不要直接修改发行版提供的 /etc/fail2ban/jail.conf,升级软件包时它可能被覆盖。新建 /etc/fail2ban/jail.d/sshd.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
findtime = 10m
maxretry = 5
bantime = 1h
bantime.increment = true
bantime.maxtime = 1w
[sshd]
enabled = true
port = ssh
backend = systemd
如果 SSH 使用自定义端口,把 port = ssh 改成实际端口,例如 port = 2222。backend = systemd 直接读取 journal,不应再同时设置 logpath。
Fail2Ban 会使用发行版设置的默认封禁动作。如果希望在 firewalld 的 rich rules 中直接看到封禁,可在 [sshd] 下增加:
banaction = firewallcmd-rich-rules
如果网卡不在 public zone,应先检查本机 /etc/fail2ban/action.d/firewallcmd-common.conf 的 zone 设置并按实际环境覆盖。Fail2Ban 官方建议把本地修改放在 .local 文件中,而不是修改随软件发布的 .conf 文件,详见 jail.conf 手册;firewalld 动作的实现可查看 firewallcmd-rich-rules.conf。
6.3 验证并启动
# 检查配置,修复所有错误后再启动
sudo fail2ban-client -t
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
# 查看所有 jail 和 sshd jail 状态
sudo fail2ban-client status
sudo fail2ban-client status sshd
持续观察封禁日志:
sudo journalctl -u fail2ban -f
也可以通过 Fail2Ban 手工封禁或解封一个地址,用于验证整条防火墙动作链:
sudo fail2ban-client set sshd banip 203.0.113.10
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 203.0.113.10
测试时使用一个确定不是当前管理端的地址。不要在唯一可用的 SSH 来源上故意连续输错密码。
7. 日常检查清单
完成初始化后,至少确认以下项目:
who -uH、w和loginctl list-sessions中没有陌生会话。- 普通管理员账户可以通过密钥新建 SSH 会话并正常使用 sudo。
sudo sshd -t无输出,sudo sshd -T显示 root 和密码登录已按预期关闭。- firewalld 或 UFW 只启用一套,云厂商安全组也只开放必要端口。
fail2ban-client status sshd显示 jail 正常运行,并且日志中没有 backend 或防火墙错误。- 系统安全更新、时间同步和日志持久化正常,云控制台救援入口可用。
总结
云服务器安全不是单个 SSH 配置项,而是一条连续的防线:先看清当前用户和连接,再用普通账户、密钥和可选 2FA 降低认证风险,通过 SSH 配置缩小入口,用防火墙处理紧急来源,最后由 Fail2Ban 自动响应持续的暴力破解。所有可能中断远程连接的修改都应遵循”保留旧会话、检查语法、新开终端验证”的顺序。