拿到一台新的云服务器后,不要立刻关闭 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

whow 读取登录会话记录,适合回答”目前有哪些用户登录”;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

这里必须使用 >> 追加公钥;使用 > 会覆盖已有公钥。目录权限应为 700authorized_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 注册。

SSH 二次验证

5. 用防火墙临时封禁登录来源

手工封禁适合正在发生的攻击或应急处置;持续的暴力破解应交给下一节的 Fail2Ban。执行前先通过 whowss$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 接受秒数以及 smh 后缀。这个规则只存在于运行时配置,过期后自动移除,不能和 --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 firewalldufw 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 = 2222backend = 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. 日常检查清单

完成初始化后,至少确认以下项目:

  1. who -uHwloginctl list-sessions 中没有陌生会话。
  2. 普通管理员账户可以通过密钥新建 SSH 会话并正常使用 sudo。
  3. sudo sshd -t 无输出,sudo sshd -T 显示 root 和密码登录已按预期关闭。
  4. firewalld 或 UFW 只启用一套,云厂商安全组也只开放必要端口。
  5. fail2ban-client status sshd 显示 jail 正常运行,并且日志中没有 backend 或防火墙错误。
  6. 系统安全更新、时间同步和日志持久化正常,云控制台救援入口可用。

总结

云服务器安全不是单个 SSH 配置项,而是一条连续的防线:先看清当前用户和连接,再用普通账户、密钥和可选 2FA 降低认证风险,通过 SSH 配置缩小入口,用防火墙处理紧急来源,最后由 Fail2Ban 自动响应持续的暴力破解。所有可能中断远程连接的修改都应遵循”保留旧会话、检查语法、新开终端验证”的顺序。