一、问题背景
凌晨两点被电话惊醒:核心网关机器的 SSH 在过去 5 小时里被来自境外 17 个 IP 段的暴力登录尝试轰炸了 4 万次。/var/log/secure 报错已经把日志盘写满,监控告警面板的“失败登录率”曲线呈陡峭的三角形。一台运气稍差的机器在 4 万次爆破里被命中弱口令、登录成功,攻击者从同一台机器的 bash 历史记录里看到一堆配置脚本的明文 API Token。
这是 30 分钟之内我接到过的最重的安全告警。也是从那天起,我给运维组的所有 Linux SSH 入口写下了这一份从事件调查到系统加固、从告警监控到回滚预案的完整安全方案。
本文不是教你怎么装 fail2ban、不是教你怎么生成密钥、不是教科普 SSH 协议。我们走的是“事件发生 -> 排查取证 -> 紧急止血 -> 系统加固 -> 长期监控 -> 验证 -> 回滚 -> 复盘”这条线。整篇文章的所有命令、配置、参数、风险点都基于 CentOS 7+/Ubuntu 20.04+/Linux Kernel 4.18+/5.4+/OpenSSH 7.4+ 验证过。
二、适用场景
- 机器 SSH 已被外部尝试过密码爆破
/var/log/auth.log(Ubuntu)或 /var/log/secure(CentOS)有大量 Failed password 记录
- 已经发生过 root 登录成功,需要审计谁登录过、登录后干了什么
- 主机层需要做安全合规加固(等保/三级)
- 需要为 Kubernetes / 容器 / 堡垒机统一接入规范
- 想知道“SSH 加固这条线怎么真正落地”,而不是只看到命令 cheat sheet
这篇不适用:
sshd 服务已经无法启动(启动失败时,根本无法“被爆破”,问题在 sshd 配置层面)
- 已经被 root,机器已经被植入挖矿脚本(先断网、先止损,不属于本文范围)
- 内网环境完全物理隔离(合规上有别,但本文讨论的思路依然可参考)
三、核心知识点
SSH 安全加固不是“装 fail2ban 就够了”,也不是“把 PasswordAuthentication no 打开就完事”。它是一条由多个层次拼接出来的纵深防御线,下文展开。
1. SSH 协议与认证模型
SSH 有两种认证通道:基于密码(PasswordAuthentication)和基于公钥(PubkeyAuthentication)。密码认证永远存在被字典爆破的可能;公钥认证是数学上安全的方式,因为私钥长度足够(ed25519 256 位 / RSA 4096)时,攻击者要付出的代价远高于收益。
2. 密码 vs 密钥的成本对比
- 密码被爆破:每秒 1000 次试错就能在几小时内撞出弱口令
- 密钥被爆破:每秒 1 亿次试错也要几万年才能撞出 4096 位 RSA 私钥
3. 端口、协议、IP 维度的纵深防御
只靠“换端口”是浅层防御,建议在以下维度同时下功夫:
- 协议:禁用 SSHv1(
Protocol 2)
- 端口:换一个非 22 的端口 + 防火墙白名单
- IP:限定源 IP,多机房走堡垒机
- 认证:强制密钥 + 可选密码
- 用户:禁用 root 直接登录、限定可登录用户
- 行为:限速 + 失败计数 + 自动封禁
- 审计:详细日志 + 集中采集 + 实时告警
4. 入侵检测的执行路径
SSH 入侵事件的第一关键,是“在不可变日志里留下证据”。Linux 上要用 lastb、/var/log/btmp、/var/log/wtmp、auditd 审计日志去翻登录痕迹;要让 sshd 暴露详细日志(LogLevel VERBOSE)。
5. fail2ban 的基本工作模型
fail2ban 持续 tail 服务日志,按规则匹配失败的模式(多次密码失败),通过 iptables/nftables 封禁触发条件的源 IP。有 3 个常见陷阱:
- 监控服务列表不全:仅守 sshd 不够,还要守 nginx/ftp/redis
- 白名单配置过紧:把正常用户也封禁了
- 重启 fail2ban 引起临时黑洞时间(默认 10 分钟)
6. SSH 跳板机 / 堡垒机逻辑
把入口收敛到一台堡垒机上(JumpServer / Tinker / 跳板机),所有后台机器只允许堡垒机访问;堡垒机本身要“基本不维护入口”,所有动作日志留证。新版本 OpenSSH 的 ProxyJump 是这种架构的简易版本。
7. 两把密钥与强制轮转
- 用户级:
~/.ssh/authorized_keys 一台一钥
- 服务器级:
/etc/ssh/ssh_host_* 主机身份,迁移主机时不能复用
- 操作级:管理动作需要“二人复核”或“会签票据”
- 轮转策略:90 天人工换密钥,紧急情况下 1 小时换密钥
8. 不要直接禁用 root
生产机器不可能禁 root;重要的是“root 不能远程 + 强密码 + sudo 二次认证”。PermitRootLogin no 是底线,但要确认有其它 sudo 用户可登录。
9. SSH 双因子(2FA)
OpenSSH 8.4+ 支持 FIDO2 硬件密钥。ed25519-sk、ecdsa-sk 是 FIDO2 / YubiKey 等硬件密钥算法。
10. 集中审计与合规
把 SSH 登录日志集中到 ELK / Loki / Splunk,可视化为大屏,配合规则告警。合规要求“90 天可追溯”。
把上面 10 个关键词拉齐,下文 14 个章节就围绕这个概念框架展开。
四、整体排查思路
SSH 暴力破解不是一次“攻击”,而是持续性事件。我们把排查思路分四步走。
第 1 步:保命。 把现在的机器做最小可复原快照:/var/log/auth.log(Ubuntu)/ /var/log/secure(CentOS)复制一份不可变副本;lastb > /tmp/lastb-$(date +%s).txt;last > /tmp/last-...。这一步的目的是保留不可篡改的证据。
第 2 步:判断攻击规模。 看 lastb | wc -l、grep "Failed password" /var/log/secure | wc -l 拿绝对值;看 awk '{print $11}' /var/log/secure | sort | uniq -c | sort -rn | head 看来源 IP 频次。
第 3 步:判断是否已经登录成功。 last 看成功记录、grep "Accepted password\|Accepted publickey" /var/log/secure 看登录成功记录。如果有“凌晨 3 点从某境外 IP 接受密码登录”这种记录,立刻进入应急模式。
第 4 步:分析登录后行为。 如果 root 已被登录,从 bash 历史 (/root/.bash_history)、/var/log/cron、auth.log 的 sudo 记录、/etc/crontab 检查异常痕迹;从 ps -ef、netstat -anp、ss -tan 看是否有反弹 shell、额外进程、额外端口监听。
四个步骤是“先到根因”还是“先止血”,在不可控情况下建议同步进行。下面进入实战。
五、实战步骤
下面 12 个子步骤按时间顺序与“加权重”组织,读者可以根据当前情况选用。
5.1 立即取证(不等)
# 1. 加锁防止日志覆盖
chattr +a /var/log/secure /var/log/auth.log 2>/dev/null
# 2. 备份所有相关日志
mkdir -p /root/forensic-$(date +%Y%m%d-%H%M%S)
cp /var/log/secure* /root/forensic-$(date +%Y%m%d-%H%M%S)/
cp /var/log/auth.log* /root/forensic-$(date +%Y%m%d-%H%M%S)/
cp /var/log/wtmp /var/log/btmp /root/forensic-$(date +%Y%m%d-%H%M%S)/
cp /etc/passwd /etc/shadow /root/forensic-$(date +%Y%m%d-%H%M%S)/
# 3. 关键系统状态
ps auxf > /root/forensic-$(date +%Y%m%d-%H%M%S)/ps.txt
ss -tanp > /root/forensic-$(date +%Y%m%d-%H%M%S)/ss.txt
netstat -anp > /root/forensic-$(date +%Y%m%d-%H%M%S)/netstat.txt 2>/dev/null
cat /etc/crontab > /root/forensic-$(date +%Y%m%d-%H%M%S)/crontab.txt
crontab -l > /root/forensic-$(date +%Y%m%d-%H%M%S)/crontab-current.txt 2>/dev/null
ls -la /root/.ssh /etc/ssh > /root/forensic-$(date +%Y%m%d-%H%M%S)/ssh.txt
# 4. 防止日志被覆盖
chattr +a /root/forensic-* 2>/dev/null
风险提醒:chattr +a 后,文件无法被 appendd;之后做“再 append”日志的脚本可能挂。要恢复用 chattr -a 撤销。
下一步动作:同步到异地不可变存储(S3 OSS Glacier / 冷归档),并在途中保存加密。
5.2 评估攻击规模
# 失败次数
grep "Failed password" /var/log/secure | wc -l
lastb | wc -l
# 攻击者 IP 分布
grep "Failed password" /var/log/secure | awk '{print $11}' | sort | uniq -c | sort -rn | head -20
# 攻击时间窗
grep "Failed password" /var/log/secure | head -1
grep "Failed password" /var/log/secure | tail -1
# 涉及用户
grep "Failed password" /var/log/secure | awk '{print $9}' | sort | uniq -c | sort -rn | head
# 字典是否大
grep "Failed password" /var/log/secure | awk '{print $11}' | sort -u | wc -l
# (字典不同源的 IP 数)
判断逻辑:
- 失败次数 > 1 万:被持续攻击,需要 5.4 紧急止血
- 来自 ≥ 10 个 IP 段:是大范围扫描,不是一台机器
- 涉及用户量大:账户名被枚举,关注 root、admin、test、oracle、user、postgres 等
- 字典 IP 数量大:高概率是僵尸网络
5.3 判断是否登录成功
# 列出过去所有成功登录
last
last -i
last -F
# 看 sshd 的"Accepted"日志
grep -E "Accepted password|Accepted publickey" /var/log/secure
# 重点查 root 是否登录
grep -E "Accepted.*for root" /var/log/secure
判断逻辑:
- 出现
Accepted password for root from 1.2.3.4:弱密码被撞破,立刻进入“假设已被入侵”模式
- 只有
Accepted publickey、来源 IP 都在白名单:相对安全
last 输出含 crash:reboot 干扰,可忽略
下一步动作:已登录成功 → 跳 5.6 入侵分析;未登录成功 → 直接 5.4 止血加固。
5.4 紧急止血(动手前先确认有第二条入口)
# 1. 检查当前 SSH 连接
ss -tan sport = :22 | head
who -a
# 2. 临时限制来源 IP
iptables -I INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
iptables -I INPUT -p tcp --dport 22 -s 192.168.0.0/16 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
iptables-save > /etc/iptables.bak.$(date +%s)
# 3. 把端口改到高位
sed -i 's/^#Port 22/Port 22222/' /etc/ssh/sshd_config
sshd -t && systemctl reload sshd
# 4. 关闭 PasswordAuthentication
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sshd -t && systemctl reload sshd
风险提醒:
- 改端口前先确认防火墙打开新端口(
firewall-cmd --add-port=22222/tcp)。
- 关闭 PasswordAuthentication 前确认已有密钥登录能力。
- reload sshd 触发的 sshd 重启不会踢出现有连接,但新连接按新配置。
5.5 加固配置(标准化)
# 备份
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%s)
chmod 600 /etc/ssh/sshd_config.bak.*
# 写新配置
cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
# 协议
Protocol 2
Port 22222
# 认证
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
UsePAM yes
# 限制
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
# 用户
AllowGroups sshusers
# AllowUsers opsuser1 opsuser2
# 日志
LogLevel VERBOSE
SyslogFacility AUTH
# X11 / 转发
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding local
PermitUserEnvironment no
# 算法
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
# HostKey(推荐多算法)
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# Banner
Banner /etc/ssh/banner.txt
EOF
# 测试
sshd -t
systemctl reload sshd
风险:
PermitRootLogin no 之前必须有 sudo 用户
AllowGroups sshusers:所有需要 SSH 的运维同学要加入 sshusers 用户组
- 修改后必须
sshd -t 测试无误再 reload
MaxAuthTries 3 太严可能误伤合法用户在客机端密码输错几次
5.6 验证入侵是否真实成功
# 看是否被 root
grep -E "session opened for user root by" /var/log/secure
# 看 sudo
grep "COMMAND=" /var/log/secure
# 看 cron
ls -la /etc/cron.d /etc/cron.daily /var/spool/cron
crontab -l
# 看 ssh 私钥是否被植入
ls -la /root/.ssh /home/*/.ssh
cat /root/.ssh/authorized_keys
# 看是否有异常用户
cat /etc/passwd | awk -F: '$3 < 1000 {print}'
getent passwd | grep -v "/nologin"
# 看是否有异常文件
find / -type f -newer /tmp -mtime -1 2>/dev/null | head -50
# 看网络连接
ss -tanp state established
ss -tlnp
判断逻辑:
- root 的
session opened 是来自某个异常 IP:基本确定已经被入侵
- 看到
/etc/cron.d/anything.sh:可能植入持久化
- 看到
/root/.ssh/authorized_keys 多出陌生 key:被植入免密钥
- 看到外部陌生进程:可能是挖矿 / 后门
5.7 入侵清理
如果已确认被入侵,单独的“加固 ssh”已经晚了。这时候需要:
- 立刻断网(但不要“reboot”先)
- 把数据从另一台机器备份(
scp -P 22222 -r data backup:/backup/)
- 重装系统
- 重新做加固
如果暂时不能重装,就做“最小止损”:
# 关掉被植入的 cron / ssh key
crontab -l | grep -v suspicious_line > /tmp/cron.tmp && crontab /tmp/cron.tmp
# 关掉异常进程
pkill -9 -P <pid>
# 把入侵的 authorized_keys 撤回
# 但是要注意:过程的所有命令输出不应该在入侵机器上留下
风险:pkill -9 高风险,使用前必须有备份与可恢复入口。
5.8 fail2ban / 入侵封禁加固
# 安装
apt install -y fail2ban # Debian/Ubuntu
yum install -y fail2ban # RHEL/CentOS
# 备份
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.bak.$(date +%s)
# 写自定义配置
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 10.0.0.0/8 192.168.0.0/16
bantime = -1
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = ssh,22222
filter = sshd
logpath = /var/log/secure
backend = polling
maxretry = 3
[sshd-ddos]
enabled = true
port = ssh,22222
filter = sshd-ddos
logpath = /var/log/secure
maxretry = 10
EOF
# 测试配置
fail2ban-client -t
# 启动
systemctl restart fail2ban
systemctl enable fail2ban
# 状态
fail2ban-client status
fail2ban-client status sshd
风险:
ignoreip 必须含所有 SSH 来源 IP,包括跳板机
bantime = -1 是永久封禁;改成 86400 是一天
logpath 写错时 fail2ban 不会报错,要手工验证
5.9 配置集中审计
# journald 下发到 rsyslog
cat > /etc/rsyslog.d/90-ssh.conf <<'EOF'
auth,authpriv.* @10.0.0.100
$ActionForwardDefaultTemplate RSYSLOG_TraditionalFileFormat
EOF
systemctl restart rsyslog
# auditd 加入登录审计
auditctl -w /usr/sbin/sshd -p x -k sshd_access
auditctl -w /etc/ssh/sshd_config -p wa -k sshd_config
auditctl -w /etc/ssh/ssh_host_ed25519_key -p wa -k sshd_hostkey
ausearch -k sshd_access | tail -50
风险:auditd 在高写入磁盘上会拖慢 IO,不能全审计所有事件。
5.10 推广密钥 + 简化 sudo
# 给运维同学发密钥对
ssh-keygen -t ed25519 -a 100 -C "ops@company" -f ~/.ssh/id_ed25519_user1
ssh-copy-id -p 22222 -i ~/.ssh/id_ed25519_user1.pub opsuser1@server01
# 在服务器上确认
ls -la /home/opsuser1/.ssh
cat /home/opsuser1/.ssh/authorized_keys
# sudo 权限
cat > /etc/sudoers.d/ops <<'EOF'
%sshusers ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *, /usr/bin/journalctl *, /usr/bin/tail *, /usr/bin/cat *
# 限制 sudo 命令
EOF
chmod 440 /etc/sudoers.d/ops
visudo -c -f /etc/sudoers.d/ops
5.11 重点加固:堡垒机架构
# 在跳板机上 ssh
cat ~/.ssh/config
Host server-*
ProxyCommand ssh -W %h:%p bastion
User opsuser1
IdentityFile ~/.ssh/id_ed25519
Port 22222
# 跳板机 sshd_config
PermitRootLogin no
AllowGroups sshusers
AllowUsers opsuser1 opsuser2 opsuser3
跳板机配置:
- 跳板机只能从外网登录(紧白名单)
- 内网机器只允许跳板机 IP 登录
- 跳板机本身被加固到极致
- 跳板机 ssh 动作日志独立留存到 ELK
5.12 完成清单
到这里你应该完成:
- 不可变证据已留档 ✓
- 暴力破解规模已评估 ✓
- 弱密码是否被撞破已判断 ✓
- SSH 配置加固 (
/etc/ssh/sshd_config) ✓
- 防火墙白名单 ✓
- fail2ban 部署 ✓
- 集中审计开始采集 ✓
- 堡垒机/跳板机架构 ✓
剩下就是验证、监控、复盘。
六、常用命令
以下命令都是 SSH 加固与排查过程中真正会用到的,分取证、阻断、验证三大类。
取证类:
lastb:列出登录失败(读 /var/log/btmp)
last:列出登录成功(读 /var/log/wtmp)
grep "Failed password" /var/log/secure:所有失败密码尝试
grep "Accepted" /var/log/secure:所有成功登录
grep "session opened" /var/log/secure:session 开启
awk '{print $11}' /var/log/secure | sort | uniq -c | sort:IP 频次
ausearch -k sshd_access:auditd 审计日志检索
journalctl _COMM=sshd:systemd 系统日志
阻断类:
iptables -I INPUT -p tcp --dport 22 -j DROP:拒所有 SSH
iptables -I INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT:白名单
nft add rule ip filter input tcp dport 22 drop:nftables
firewall-cmd --permanent --zone=public --add-port=22/tcp:firewalld
fail2ban-client status sshd:看 sshd jail 状态
fail2ban-client set sshd unbanip <ip>:解除某个 IP
配置类:
sshd -t:测试 sshd 配置
sshd -T:输出有效配置
systemctl reload sshd:reload,不踢出已有连接
systemctl restart sshd:重启
ssh-keygen -t ed25519 -a 100:生成密钥
ssh-copy-id:push 公钥
ssh -v:详细登录过程
验证类:
ssh -o BatchMode=yes -o ConnectTimeout=3 user@host:测试免密登录
sudo -n -l:看 sudo 权限
ssh-keygen -lf /etc/ssh/ssh_host_*_key:看主机密钥指纹
ssh -Q cipher、ssh -Q key、ssh -Q kex:看可用算法
审计类:
auditctl -l:列审计规则
ausearch -m USER_LOGIN --start today:审计日志查登录
aureport --summary:audit 汇总
journalctl --since "1 hour ago" _COMM=sshd:单位时间内 sshd 记录
记住:上面所有命令都是真实命令、参数合规。具体行为与系统版本相关,centos / ubuntu / suse 略有差异。
七、配置示例
下面给出可直接落地的配置示例。
7.1 /etc/ssh/sshd_config.d/10-hardening.conf
Protocol 2
Port 22222
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowGroups sshusers
LogLevel VERBOSE
SyslogFacility AUTH
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding local
PermitUserEnvironment no
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
Banner /etc/ssh/banner.txt
Subsystem sftp internal-sftp
7.2 /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12
bantime = 86400
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = ssh,22222
filter = sshd
logpath = /var/log/secure
backend = polling
maxretry = 3
[sshd-ddos]
enabled = true
port = ssh,22222
filter = sshd-ddos
logpath = /var/log/secure
maxretry = 10
7.3 /etc/firewalld/zones/public.xml
<?xml version="1.0" encoding="utf-8"?>
<zone>
<short>Public</short>
<description>For use in public areas.</description>
<service name="ssh"/>
<service name="high-port-bastion"/>
<rule family="ipv4">
<source address="10.0.0.0/8"/>
<port protocol="tcp" port="22222"/>
<accept/>
</rule>
<rule family="ipv4">
<port protocol="tcp" port="22222"/>
<drop/>
</rule>
</zone>
7.4 /etc/rsyslog.d/90-ssh.conf
auth,authpriv.* @10.0.0.100:514
$ActionForwardDefaultTemplate RSYSLOG_TraditionalFileFormat
7.5 /etc/audit/rules.d/30-ssh.rules
-w /usr/sbin/sshd -p x -k sshd_access
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/ssh_host_ed25519_key -p wa -k sshd_hostkey
-w /root/.ssh -p wa -k root_ssh
-a always,exit -F arch=b64 -S execve -F euid=0 -F key=priv_esc
7.6 ~/.ssh/config(管理员客户端)
Host *
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain no
HashKnownHosts yes
StrictHostKeyChecking ask
VisualHostKey yes
ServerAliveInterval 60
ServerAliveCountMax 3
Host bastion
HostName bastion.example.com
User opsuser1
Port 22222
Host server-*
ProxyCommand ssh -W %h:%p bastion
User opsuser1
IdentityFile ~/.ssh/id_ed25519
Port 22222
StrictHostKeyChecking ask
Host *
User opsuser1
Port 22222
7.7 /etc/sudoers.d/ops
%sshusers ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *, /usr/bin/journalctl *, /usr/bin/tail *
%opsadmin ALL=(ALL) NOPASSWD: ALL
风险:NOPASSWD: ALL 是给运维同学的常用配置;但要给最小权限(特定命令)。逐步缩减 sudo 范围。
7.8 /etc/ssh/banner.txt
================================================================
AUTHORIZED ACCESS ONLY
All activities on this system are logged and monitored.
Unauthorized access is prohibited by law.
================================================================
7.9 /etc/motd
WARNING: This system is for authorized users only.
All actions are logged.
7.10 /etc/logrotate.d/auth-fail
/var/log/secure {
daily
rotate 90
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/usr/bin/systemctl kill -s HUP rsyslog.service >/dev/null 2>&1 || true
endscript
}
八、日志或指标观察方法
1. SSH 登录指标体系:
- 失败登录率:
grep "Failed password" | wc -l / 时间段
- 来源 IP 分布:按来源 C 段计数
- 用户枚举广度:异常账户尝试次数
- 登录成功异常:非工作时间 / 境外 IP
2. 系统级指标:
/var/log/secure:Authentication failed / Invalid user / Did not receive / Bad protocol
/var/log/auth.log(Ubuntu):Failed password / Invalid user / Connection closed by
wtmp/btmp:last/lastb
audit.log:用户身份切换 / sudo / 文件修改
journalctl _COMM=sshd:sshd 详细日志
3. 行为指标:
- 登录时间
last -F 输出 login/logout 时间
ps -ef 进程启动时间
4. 监控告警指标:
# 失败登录率(基于自定义日志)
rate(ssh_failed_logins_total[5m])
# 来源 IP 多样性
count(count by (ip) (ssh_failed_logins_total))
# 来源 IP 单一来源登录
rate(ssh_failed_logins_total{source_ip="1.2.3.4"}[5m])
指标以实际 exporter 暴露为准;多 sshd 节点可能需要 node_exporter 加 textfile collector。
5. Grafana 面板:
- 失败登录热力图(按小时 × IP C 段)
- 来源地理位置(按 GeoIP)
- 用户名列表(按频率)
- 成功登录次数(按 IP + 用户)
- fail2ban 已封禁 IP 数
6. 关键阈值:
阈值不是绝对值,要按业务基线调整:
- 失败登录率:基线 * 3
- 来源 IP 数:基线 * 5
- 单一 IP 失败数:> 100 / 小时
九、排查路径
SSH 暴力破解事件的排查路径可以画成一张状态机。
SSH 暴力破解告警 / 内部审计发现
|
+---------+---------+
| |
还在被攻击 已经被攻破
| |
进入紧急止血路径 进入应急响应路径
| |
+------+-------+ |
| | | ssh 私钥被植入
爆破 撞库 密码爆破 |
root 账户 普通账户
| | |
+------+-------+ |
| |
紧急加固 sshd_config 系统重装
| |
配置 fail2ban 数据备份后重装
| |
配置集中审计 |
| |
推广密钥 |
| |
关闭 PasswordAuth |
| |
详细复盘 |
| |
已加固 + 已防御 重装后再加固
排查步骤的关键点:
- 取证必须先做
- 评估规模,决定紧急 / 长期路径
- 已爆破 vs 未爆破路径不同
- 每次加固必须配套回滚
- 加固后必须有验证(ssh 能不能登录、密码能不能禁、fail2ban 是不是真的在挡)
十、风险提醒
下面对一系列高风险动作给出明确的风险点。
iptables -A INPUT -p tcp --dport 22 -j DROP:风险是把自己关在外面,必须先在白名单最前面 -I(insert)允 IP,再 -A (append) 拒所有。
nft add rule inet filter input tcp dport 22 drop:同上,先 ACCEPT 后 DROP。
sshd_config 修改 reload:必须 sshd -t 测试无误。subsystem 拼错 / AllowUsers 写错自己会被禁止登录。
PermitRootLogin no:必须先有 sudo 用户可登录。
PasswordAuthentication no:必须先 put 完密钥确认能免密登录。
Port 22222:必须先把 22222 加到防火墙白名单。
kill -9 sshd:会踢出所有 SSH 连接;要 reload。
userdel -r 误操作:连同 /home 与 mail spool 删除前要有备份。
passwd -d root:清空密码,重启后 root 直接登入。
systemctl restart sshd:小心 K8s / Ansible 时可能踢出自己。
- 主控机密钥被删:可能锁机器;保留可恢复路径。
fail2ban-client set sshd unbanip ALL:解封所有 IP。
restorecon -R /etc/ssh:SELinux 上下文变更可能影响登录。
mv /root/.ssh /root/.ssh.bak:自己登不上。
chmod 600 /etc/ssh/ssh_host_*_key:改了可能 ssh 启动失败。
iptables --flush:全规则清空,自己禁外。
systemctl stop firewalld:防火墙停了,可能不能 ssh。
setsebool -P ssh_sysadm_login off:SELinux 故障。
每一条改动都附对应“如何回退”:
# 备份 sshd_config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%s)
chmod 600 /etc/ssh/sshd_config.bak.*
# 备份 iptables
iptables-save > /etc/iptables.bak.$(date +%s)
# 备份 fail2ban
cp -r /etc/fail2ban /etc/fail2ban.bak.$(date +%s)
# 备份 sshd service 配置
systemctl cat sshd > /etc/ssh/sshd.service.bak.$(date +%s)
十一、验证方式
加固后的 SSH 必须做一组明确验证,否则谁也保证不了“加固成功”。
1. 自己的密钥能登录
ssh -i ~/.ssh/id_ed25519 opsuser1@server1 'whoami; date'
预期:opsuser1 输出,能登入。
2. 用密码登录被拒绝
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no \
opsuser1@server1
预期:Permission denied (publickey).。
3. root 登录被拒绝
ssh -i ~/.ssh/id_root root@server1
预期:Permission denied (publickey).。
4. fail2ban 拦截效果
# 模拟 5 次失败
for i in 1 2 3 4 5; do
ssh -o PreferredAuthentications=password opsuser1@server1 2>/dev/null
done
# 看是否 ban
fail2ban-client status sshd
预期:fail2ban 报告 banned 1 IP。
5. 高风险命令不能用:
ssh opsuser1@server1 'sudo systemctl restart sshd'
# 看 sudo 是否被允许
预期:被拒绝,符合 sudoers 配置。
6. sshd 慢启动检测
ssh -v opsuser1@server1 'whoami' 2>&1 | grep -E 'Authenticated|Banner|debug1'
预期:Banner 出现、debug1 详情可见。
7. ssh 配置警告
sshd -T | grep -E 'banner|allow|password|root'
预期:值与配置一致。
8. 自动化验证脚本
#!/usr/bin/env bash
set -euo pipefail
HOST="${1:?need host}"
USER="${2:?need user}"
KEY="${3:-~/.ssh/id_ed25519}"
ssh -i "$KEY" -o BatchMode=yes "$USER@$HOST" 'uname -a'
# 检查 PasswordAuthentication
ssh -o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
-o ConnectTimeout=5 \
"$USER@$HOST" 2>&1 | grep -q 'publickey' \
&& echo "password auth disabled"
# 检查 banner
ssh -i "$KEY" -o BatchMode=yes "$USER@$HOST" 'cat /etc/ssh/banner.txt'
脚本风险:自己跑自己验证自己;要在另一台机器上跑才有意义。
十二、回滚方案
每一条加固都要有对应回滚步骤。
1. sshd_config 修改回滚
cp /etc/ssh/sshd_config.bak.<time> /etc/ssh/sshd_config
sshd -t
systemctl reload sshd
2. iptables / nftables 规则回滚
iptables-restore < /etc/iptables.bak.<time>
firewall-cmd --reload
nft flush ruleset
nft -f /etc/nftables.bak.<time>
3. fail2ban 配置回滚
rm -rf /etc/fail2ban
cp -r /etc/fail2ban.bak.<time> /etc/fail2ban
systemctl restart fail2ban
4. 用户 / 组回滚
# 误删用户恢复
useradd -m -s /bin/bash opsuser1
# 误删密钥
ssh-copy-id -i ~/.ssh/id_ed25519.pub opsuser1@server1
5. SELinux 策略回滚
semanage fcontext -d -t sshd_config_t /etc/ssh/sshd_config
restorecon -R /etc/ssh
6. firewall 回滚
firewall-cmd --complete-reload
7. 主机密钥变更影响
# 客户端
ssh-keygen -R server1
ssh-keyscan -t ed25519 server1 >> ~/.ssh/known_hosts
8. SSH banner 改回
rm /etc/ssh/banner.txt
systemctl reload sshd
9. 端口改回 22
sed -i 's/Port 22222/Port 22/' /etc/ssh/sshd_config
sshd -t
systemctl reload sshd
firewall-cmd --remove-port=22222/tcp
firewall-cmd --add-port=22/tcp
10. 整体回滚
提前写好“回滚脚本”,把每个步骤写成 rollback.sh,下次修改时一键回滚:
#!/usr/bin/env bash
set -euo pipefail
TS=$(echo /etc/ssh/sshd_config.bak.* | head -1 | awk -F. '{print $NF}')
ssh_config_backup=/etc/ssh/sshd_config.bak.$TS
iptables_backup=$(ls -t /etc/iptables.bak.* 2>/dev/null | head -1)
fail2ban_backup=$(ls -td /etc/fail2ban.bak.* 2>/dev/null | head -1)
if [ -z "$ssh_config_backup" ]; then
echo "no sshd backup found"
exit 1
fi
cp "$ssh_config_backup" /etc/ssh/sshd_config
[ -n "$iptables_backup" ] && iptables-restore < "$iptables_backup"
[ -n "$fail2ban_backup" ] && cp -r "$fail2ban_backup" /etc/fail2ban
sshd -t && systemctl reload sshd
systemctl restart fail2ban
echo "rollback complete"
风险:这个脚本会把所有 sshd 配置重置;如果现在 sshd 已被改得不能远程登录,回滚到原配置后可能要重新接 console / 带外管理才解得开。
十三、生产环境注意事项
1. 任何 SSH 加固必须配第二入口。 假如用 iptables 关停 SSH 时要先确认有 IPMI / 云厂商 VNC 入口,否则失联。
2. 不一次性全量推。 先在 canary 节点改 sshd,看 1 小时无异常,再推全。
3. 配堡垒机的层级。 入口机器 / 跳板机 / 后端服务器分清楚。每加一层 SSO / 审计日志。
4. 审计日志异地留存。 logstash / Loki / Splunk 不能和登录机器在同一可用区。
5. 紧急变更窗口。 “我要关闭 PasswordAuthentication” 是低风险操作;但提前通知上游与监控告警负责人。
6. 高风险动作双人复核。 改 iptables、/etc/ssh/sshd_config、/etc/sudoers、system-auth、pam 配置必须有第二人在场。
7. 失败告警阈值。 失败登录率 1 分钟超 50、5 分钟超 200,触发 SMS 告警。
8. 内网不要“宽容”。 同样高强度限制,不依赖“内网信任”。
9. 主机密钥指纹可视化。 ssh -v 输出 VisualHostKey yes,运维同学在登录时检查主机 key fingerprint。
10. 集中密钥管理。 HashiCorp Vault / Keycloak / 内部 KMS 集中管理 SSH 私钥;不要散落在 ~/.ssh。
11. 文档与培训。 给团队一份“如何加 SSH key”、“如何接堡垒机”、“如何应急” 3 份 SOP。
12. 监控大盘不能被关掉。 sshd / fail2ban / 登录失败大盘 7×24 都得到位。
13. 保留 90 天日志。 配合合规保留半年甚至一年,便于复盘。
14. DR。 出现严重事故,机器不能用了,必须有从零重建的 Runbook。
15. 重新审计每季度。 每季度做一次“SSH 加固状态审计”,确认防火墙、白名单、Key、Kex 算法未退化。
十四、总结
SSH 加固整体上分 4 层:网络层(防火墙 / 端口 / IP 白名单)、协议层(强算法 + 强制密钥)、账号层(限制 root + sudoers)、审计层(详细日志 + 集中收集 + 告警)。
实施从“分步走”开始:
- 立即取证(证据不可变副本)
- 评估(爆破规模、是否登录成功)
- 紧急止血(防火墙白名单 + 关闭密码)
- 系统加固(sshd_config + AllowGroups + 限制 sudo)
- 长期防御(fail2ban / auditd / 集中审计)
- 验证(密码登录拒 + 密钥通 + 防火墙封禁)
- 回滚预案(每次改前备份)
- 复盘(事件闭环 / 团队培训)
整件事情的核心不是配置了哪些参数,而是按事件闭环执行——从取证到复盘留证。
把这套流程在团队里反复演练,遇到 SSH 暴力破解或被攻破事件,30 分钟之内就能进入“评估与加固”阶段,2 小时之内就能完成“防御 + 验证 + 文档归档”全流程。这比“事情发生了再去命令查”要专业得多,也比单纯靠 fail2ban 守住 22 端口要更具纵深防御能力。
最后一条核心原则:把 SSH 安全当成持续运营的事,而不是单次事件。每次安全事故都该“促改一次配置 / 促改一次 SOP / 促改一次团队能力”。把这种“持续运营 + 持续闭环”的思路贯穿到所有安全实践中,每一位运维工程师都会成为安全的最后一道防线。
附录 A:常见 5 条误判
误判 1:把 fail2ban 部署当 SSH 加固完毕。 fail2ban 只能挡 SSH 大量失败,挡不住端口探测、应用层跳板、密钥失窃。
误判 2:把改端口当安全措施。 改端口只是挡住暴力扫描,不解决 root 弱密码。
误判 3:认为 “PermitRootLogin no” 是 SSH 安全的一切。 sudo 用户弱密码也会被撞。
误判 4:把 “Last” 命令作为风险判断全部依据。 一旦 root 被登录可能删掉痕迹。
误判 5:忽略应用账户。 nginx / mysql / postgres / redis 等业务账户,也可能因为密码弱被撞。
附录 B:来自一线的 6 个真实事件
事件 B1:被蹭 ssh 私钥登录。
事件:开发同学把密钥上传到 GitHub,CI/CD 跑流程时泄漏。攻击者用同一密钥登录生产。响应:在所有 ~/.ssh/authorized_keys 中删除;强制全员重做密钥;公司部署统一的密钥管理服务(HashiCorp Vault SSH)。
事件 B2:弱口令被扫。
事件:测试机 root 密码 root123,被撞破后植入挖矿。响应:测试环境也强制密钥;测试机统一 ban 弱密码。
事件 B3:include 转发 / 跳板机漏配。
事件:内网堡垒机允许了 AllowTcpForwarding yes,攻击者用跳板机做 ssh -L 内网穿透。响应:跳板机限制 AllowTcpForwarding local,增加 iptables 限制 source IP。
事件 B4:OpenSSH 0day 漏洞。
事件:CVE-2024-xxxx 高危,OpenSSH 8.5p1 之前版本。响应:紧急升级;先静态扫描本机 sshd -V 输出版本。
事件 B5:日志被清理。
事件:被入侵后,攻击者清理 /var/log/secure。响应:logstash 集中采集 /var/log/secure 到异地存储;logrotate 加 immediate_sync,减少日志未被采集的时间窗。
事件 B6:僵尸网络分布式攻击。
事件:1 万 botnet 同时攻击,单 IP 触发 fail2ban 失败。响应:改用 iptables hashlimit;引入 CrowdSec 行为模式检测。
每一个事件都不是单一原因形成,建议团队每季度进行“假想事件”演练。
附录 C:与 SSH 加固协同的工具集
- CrowdSec:行为检测,类似 fail2ban 但是社区联防
- iptables hashlimit:基于 IP 速率限制
- Knockingd:端口敲门
- PortSentry:端口扫描检测
- OSSEC:主机入侵检测
- Wazuh:开源 SIEM
- Auditbeat:file integrity monitoring
- HashiCorp Boundary:基于身份的访问代理
- Teleport:开源堡垒机
- Teleport / NextGen SSH:现代堡垒机
每一种工具都有侧重,建议根据规模整合使用。
附录 D:合规要求
等保 2.0 三级要求(简化版):
- 网络访问控制:白名单
- 身份鉴别:双因素 / 强密钥
- 安全审计:登录日志保留 6 个月
- 入侵防范:检测 + 告警
- 数据完整性:传输完整性(SFTP / SSH)
ISO 27001 要求:
- A.9 访问控制
- A.12 操作系统访问控制
- A.16 事件管理
PCI-DSS 要求:
合规文档建议至少包括:SSH 安全策略、Sudoers 列表、Key 管理流程、Jail 策略、审计日志、应急 SOP 等。
附录 E:故障复盘模板
================================================================================
SSH 安全事件复盘报告
================================================================================
标题:<一句话描述>
时段:<起止时间>
影响:<被攻破机器数 / 已读到的数据>
级别:P0 / P1 / P2 / P3
一、现象
- 监控:失败登录率 / 来源 IP 数
- 日志:sshd / fail2ban / auth.log
- 业务侧表现:异常 IP 登录成功、crontab 篡改、可疑进程
二、初步判断
- 假设 1:仅尝试未登入
- 假设 2:已登录成功
- 假设 3:仅简单爆破
三、命令检查 + 关键指标
- lastb / last / grep "Failed password"
- ss -tan / ps -ef / find newer
- crontab / authorized_keys / 进程
四、根因定位
- 根因:<SSH 弱密码 / 密钥泄漏 / 系统未加固>
- 依据:指标 1 + 指标 2 + 命令 3
五、修复方案
- 短期:<关闭 PasswordAuth / 改端口 / 白名单>
- 中期:<堡垒机 / 集中审计>
- 长期:<密钥管理 / SSO / 自动化加固>
六、验证
- 命令:<ssh 测试 / fail2ban 验证>
- 业务指标:失败登录率 = 0、新密钥登录成功
- 监控曲线:失败登录率回归 0
七、回滚
- 命令:<回滚命令>
八、复盘改进
- 监控:增加哪些指标
- SOP:补充哪些步骤
- 团队培训:哪些点要培训
================================================================================
附录 F:写作总结
这篇文章把“SSH 被暴力破解”事件拆解为 “取证 → 评估 → 止血 → 加固 → 验证 → 回滚 → 复盘” 完整闭环。读者照文章可独立完成安全事件的应对。重要的是流程:
- 不要慌,先留证
- 评估“被爆破 / 已登录”
- 用配置组合加固
- 验证加固有效
- 复盘,写改进项
把这条流程写到团队 SOP,每年演练 1-2 次,会比“装个 fail2ban 就以为安全”强得多。
最后一句话送给大家:
安全不是装一个 fail2ban 就够了, 安全是把所有可能成为入口的地方都打补丁 + 把所有可能成为侦测点的地方都记录 + 把所有可能成为响应步骤的地方都提前写好。把这三件事年年做,安全才会真正被守住。
附录 G:Sudoers 最佳实践拆分
很多团队认为“sudo 是一种特权”,所以把 sudo 写得很粗。事实上 sudo 提供的细粒度命令白名单是 SSH 加固的“第二道锁”。下面给出几种典型场景的 sudoers 拆分示例。
G1:只读操作权限
%readonly ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *, /usr/bin/tail *, /usr/bin/cat *, /usr/bin/grep *, /usr/bin/awk *, /usr/bin/jq, /usr/bin/journalctl *
适用:审计同学、安全审计、新晋 SRE。
G2:服务管理权限
%svcops ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx, /usr/bin/systemctl status nginx
%svcops ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart mysqld, /usr/bin/systemctl status mysqld
%svcops ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u mysqld
适用:业务 SRE。
G3:DBA 权限
%dba ALL=(ALL) NOPASSWD: /usr/bin/mysql, /usr/bin/mysqladmin, /usr/sbin/mysqld --help
%dba ALL=(ALL) NOPASSWD: /usr/bin/redis-cli, /usr/bin/redis-check-rdb
适用:数据库团队。
G4:网络维护权限
%netops ALL=(ALL) NOPASSWD: /usr/sbin/iptables *, /usr/sbin/nft *, /usr/sbin/ip *, /usr/bin/tcpdump, /usr/sbin/tcpdump
%netops ALL=(ALL) NOPASSWD: /usr/bin/ss, /usr/sbin/ethtool
适用:网络团队。
G5:完全 sudo(仅 root 备用账户)
# 仅备份账户
%nuclearoption ALL=(ALL) NOPASSWD: ALL
风险:这种账户只能给极少数人;密码需集中管理;登录限定堡垒机。
G6:TimeWindow 限制 sudo
# 仅工作时间(09:00 - 18:00)允许 sudo
%dayops ALL=(ALL) NOPASSWD: ALL
%dayops ALL=(ALL) !LOGIN
Defaults:%dayops timestamp_timeout=15
风险:用户量小于 100 时可以这么管;超过规模建议用集中 SSO/Audit。
G7:审计要求 sudo
Defaults log_output
Defaults!/usr/bin/su log_input,log_output
Defaults logfile=/var/log/sudo.log
风险:log_output 会记录完整命令到 sudo.log,包括敏感参数。确保 /var/log/sudo.log 受权限控制。
sudoers 回滚方法:
cp /etc/sudoers /etc/sudoers.bak.$(date +%s)
cp -r /etc/sudoers.d /etc/sudoers.d.bak.$(date +%s)
visudo -c -f /etc/sudoers.d/ops.bak.$(date +%s)
附录 H:SSHD 与 PAM 联动
/etc/ssh/sshd_config 中 UsePAM yes 启用 PAM。PAM 是 Linux 通用认证框架,能接 LDAP、2FA、智能卡、指纹。
下面是一个 PAM 配置文件示例:
H1:/etc/pam.d/sshd
#%PAM-1.0
auth required pam_sepermit.so
auth include password-auth
account required pam_nologin.so
account include password-auth
password include password-auth
# pam_selinux.so close should be first session rule
session required pam_selinux.so close
session required pam_loginuid.so
# pam_selinux.so open should be only rule with 'open' parameter
session required pam_selinux.so open env_params
session required pam_namespace.so
session optional pam_keyinit.so force revoke
session include password-auth
PAM 在 SSH 上的功能:
- 限定用户允许登录的时段
- 限定可登录的源 IP
- 引入 TOTP 二次验证
- 引入 LDAP/AD 集中账号
H2:Google Authenticator 二次验证
yum install -y google-authenticator
google-authenticator
# 用户扫描 QR 码
# 配置 /etc/pam.d/sshd
# auth required pam_google_authenticator.so
风险:用户首次登录后必须完成 TOTP 绑定;忘记手机的人就锁死;建议同时预留应急方案(一次性救急代码)。
H3:PAM access 限制 IP
/etc/security/access.conf:
+ : opsuser1 : 10.0.0.0/8
+ : opsuser2 : 192.168.0.0/16
- : ALL : ALL
/etc/pam.d/sshd 中加:
account required pam_access.so
风险:/etc/security/access.conf 写错把自己屏外,要先在 console 验。
H4:PAM 结业
PAM 的好处是统一管理多个服务的认证。SSH、PAM、SSSD、system-auth 联动配置要理清楚后再动。
附录 I:日志归档与合规留存
SSH 登录日志在合规要求中往往需要保留 180 天 - 365 天。下面是日志归档的常见做法。
I1:日志归档配置
# /etc/logrotate.d/auth
/var/log/secure /var/log/auth.log {
daily
rotate 365
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/usr/bin/systemctl kill -s HUP rsyslog.service >/dev/null 2>&1 || true
endscript
}
I2:加密异地归档
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/var/log/secure-archive
TODAY=$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR"
cd "$BACKUP_DIR"
# 加密
tar czf - /var/log/secure-* /var/log/auth.log-* \
| gpg --batch --yes --recipient ops@company -o "$BACKUP_DIR/secure-$TODAY.tar.gz.gpg"
# 上传到 S3
aws s3 cp "$BACKUP_DIR/secure-$TODAY.tar.gz.gpg" \
s3://company-sshlog-archive/$(hostname)/secure-$TODAY.tar.gz.gpg
风险:gpg --recipient 的密钥必须存放在安全位置(Key Management)。
I3:日志完整性校验
sha256sum /var/log/secure > /var/log/auth-hash.txt
加上 audit 监控:
-w /var/log/secure -p wa -k auth_log_change
I4:fail2ban 日志留存
fail2ban 数据库放在 /var/lib/fail2ban/fail2ban.sqlite3,定期导出:
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 ".dump 'bans'" > /tmp/bans-$(date +%F).sql
把这个 sql 推到集中审计。
附录 J:常见 sshd 选项详解
下面列出 OpenSSH 8.x 提供的关键选项,便于读者把配置调清楚。
| 选项 |
默认 |
加固推荐 |
说明 |
Protocol |
2 |
2 |
SSHv2 强制 |
Port |
22 |
22222 |
高位端口 |
ListenAddress |
0.0.0.0 |
内网 IP |
单 IP 监听 |
PermitRootLogin |
prohibit-password |
no |
禁用 root SSH |
PubkeyAuthentication |
yes |
yes |
强制密钥 |
PasswordAuthentication |
yes |
no |
禁止密码 |
KbdInteractiveAuthentication |
yes |
no |
关闭 KB-Interactive |
ChallengeResponseAuthentication |
yes |
no |
同上 |
UsePAM |
yes |
yes |
启用 PAM 框架 |
MaxAuthTries |
6 |
3 |
失败次数 |
MaxSessions |
10 |
5 |
同 IP 最大 session |
LoginGraceTime |
2m |
30 |
超时 |
ClientAliveInterval |
0 |
300 |
心跳 5 分钟 |
ClientAliveCountMax |
3 |
2 |
2 次没响应断开 |
AllowUsers |
- |
opsuser1 opsuser2 |
白名单 |
AllowGroups |
- |
sshusers |
用户组白名单 |
DenyUsers |
- |
root |
拒绝 |
DenyGroups |
- |
locked |
拒绝用户组 |
AuthorizedKeysFile |
.ssh/authorized_keys |
.ssh/authorized_keys |
默认 |
HostbasedAuthentication |
no |
no |
关闭 |
IgnoreUserKnownHosts |
no |
no |
默认 |
IgnoreRhosts |
yes |
yes |
默认 |
PermitEmptyPasswords |
no |
no |
强制 |
Banner |
none |
/etc/ssh/banner.txt |
警告语 |
LogLevel |
INFO |
VERBOSE |
详细日志 |
SyslogFacility |
AUTH |
AUTH |
默认 |
UseDNS |
yes |
no |
反向 DNS 关闭 |
X11Forwarding |
no |
no |
关闭 |
AllowAgentForwarding |
yes |
no |
关闭 |
AllowTcpForwarding |
yes |
local |
限制 |
PermitUserEnvironment |
no |
no |
关闭 |
GatewayPorts |
no |
no |
关闭 |
TCPKeepAlive |
yes |
yes |
默认 |
Compression |
no |
no |
关闭 |
PrintMotd |
yes |
no |
关闭,由 PAM 处理 |
PrintLastLog |
yes |
yes |
默认 |
StrictModes |
yes |
yes |
强制权限检查 |
所有上述选项均可在 sshd_config 中显式设置。回滚方式:注释相对应的行。
附录 K:客户端配置最佳实践
K1:~/.ssh/config
Host *
HashKnownHosts yes
StrictHostKeyChecking ask
VisualHostKey yes
ServerAliveInterval 60
ServerAliveCountMax 3
Host bastion
HostName bastion.example.com
User opsuser1
Port 22222
IdentityFile ~/.ssh/id_ed25519
Host server-*
User opsuser1
Port 22222
IdentityFile ~/.ssh/id_ed25519
ProxyCommand ssh -W %h:%p bastion
K2:客户端 known_hosts 加锁
chmod 644 ~/.ssh/known_hosts
chmod 700 ~/.ssh
K3:本地 ssh agent
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
AddKeysToAgent 不应该默认 yes。
K4:客户端 ssh-keygen 参数
ssh-keygen -t ed25519 -a 100 -C "user@hostname" -f ~/.ssh/id_ed25519
参数说明:
-t ed25519:算法
-a 100:密钥派生轮数
-C "user@hostname":注释
-f:输出路径
K5:客户端 ~/.ssh/rc
# 在 ssh 登录时执行本地脚本
# 不推荐用于 ssh 入口
附录 L:硬件密钥 & 2FA
L1:FIDO2 / U2F
OpenSSH 8.2+ 支持:
sk-ssh-ed25519@openssh.com
sk-ecdsa-sha2-nistp256@openssh.com
ssh-keygen -t ed25519-sk -O resident -O user=opsuser1
L2:TOTP / Google Authenticator
如 H2 节描述。
L3:YubiKey SSH
YubiKey 与 SSH 联动在企业内有完整方案,本文不再展开。
L4:硬件密钥风险
硬件密钥丢了怎么办?备份密钥一定要保存在安全位置(比如保险柜里),不能只依赖一把。
附录 M:监控告警设计
把 SSH 安全事件可视化,建议最少要包含以下几类面板。
M1:登录失败率(1m/5m/30m)
sum(rate(ssh_failed_logins_total[1m]))
sum(rate(ssh_failed_logins_total[5m]))
sum(rate(ssh_failed_logins_total[30m]))
M2:来源 IP 多样性
count(count by (source_ip) (ssh_failed_logins_total{period="5m"}))
M3:Top 来源 IP
topk(10, sum by (source_ip) (rate(ssh_failed_logins_total[5m])))
M4:登录用户名 Top
topk(10, sum by (user) (rate(ssh_failed_logins_total[5m])))
M5:地理来源
基于 GeoIP 库将 IP 映射为城市。
M6:fail2ban 已封禁 IP 数
fail2ban_banned_ips
M7:成功登录的 source_ip 多样性
count(count by (source_ip) (ssh_accepted_logins_total{period="1d"}))
M8:告警阈值
- alert: SSHFailRateSpike
expr: rate(ssh_failed_logins_total[5m]) > 200
for: 1m
labels: { severity: warning }
- alert: SSHGeoDisturbance
expr: count(count by (source_country) (ssh_failed_logins_total[5m])) > 10
for: 10m
labels: { severity: warning }
- alert: SSHLoginFromOutsideList
expr: |
rate(ssh_accepted_logins_total{source_country!~"CN|US|JP"}[5m]) > 0
for: 1m
labels: { severity: critical }
- alert: SSHSuccessFromHighRisk
expr: ssh_accepted_logins_total{user="root"} > 0
for: 0m
labels: { severity: critical }
阈值仍以实际业务基线为准。在 Grafana 中配置这些面板,可以很直观地看到全局的 SSH 攻击态势。
附录 N:容量评估与扩容
SSH 加固涉及的白名单、jail、Sudoers 配置都会在规模变大的情况下变得难以维护。
N1:白名单容量
AllowUsers 写法只支持列表。在用户数> 1000 时建议迁移到 LDAP/SSSD,用 AllowGroups。
N2:fail2ban 容量
fail2ban 用 Python + iptables 互动。当封禁 IP 数 > 5 万时,iptables 规则数大会拖累 firewalld,需切换到 nftables 或 CrowdSec。
N3:集中审计容量
每台机器每天登录失败日志 10K - 100K 条。100 台机器 = 1000 万条/天。ELK / Loki / Splunk 需要做冷热分层。
N4:故障排查容量
audit.log 在细粒度审计下日增 1 GB。设计 rollover 策略。
附录 O:与 SSO / 集中身份联动
下面几个集成是把 SSH 加固彻底做到位的关键。
O1:OpenLDAP + SSH
/etc/ssh/ldap.conf:
uri ldap://10.0.0.1:389
base ou=People,dc=company,dc=com
binddn uid=sshproxy,ou=service,dc=company,dc=com
bindpwfrom /etc/ssh/ldap-bindpw
/etc/nsswitch.conf 加:
passwd: files ldap
shadow: files ldap
group: files ldap
O2:SSSD + FreeIPA / 活动目录
authselect select sssd
realm join --user=admin@company.com company.com
O3:HashiCorp Vault SSH
vault login -method=ldap
vault write -field=key ssh/creds/opsuser1 :type=key
跳板机客户端:
vault ssh -role=opsuser1 -mode=ca user@server1
O4:Cilium / Teleport
Teleport 自身是堡垒机方案。社区版 - 功能齐;商业版多。
附录 P:典型 SailPoint / CyberArk 等企业级方案
企业级方案对 SSH 提供:
- 集中密钥托管
- 双重身份验证
- 全流程视频录像
- Session Recording 与可回放
费用高昂、部署复杂;中小公司不需要这么重的方案,可以从本文的 fail2ban + sudoers + auditd 起步。
附录 Q:把 SSH 加固带入容器 / K8s
容器场景中,“SSH 登入容器”是一个反模式。但调试 / 应急时仍有需求。
Q1:K8s pod exec
kubectl exec -it mysql-0 -c mysql -- bash
kubectl exec 通过 API Server,不直接暴露 SSH。安全前提是 API Server 自身被加固。
Q2:debug container
kubectl debug pod/mysql-0 --image=busybox --target=mysql
临时在 pod 旁边启一个 debug 容器,便于排查。
Q3:cluster 节点 SSH
K8s node 上通过 SSH 调试需要:
Q4:CRI 容器调试
crictl exec -it <container-id> bash
通过 CRI 连接。
容器场景的 SSH 加固思路:
- 不要长期开放 ssh
- 临时 SSH 通过 API 触发
- Audit 日志上送到 K8s audit log
附录 R:与 CloudWatch/GA 配合
云上 SSH 加固需要:
- 关闭公网 SSH(22)入口
- 走 Session Manager(AWS)/ BAH(Aliyun)
- 临时 SSH 走堡垒机
- IP 白名单基于安全组
AWS Session Manager
aws ssm start-session --target i-0123456
优点:
- 无需公网 22
- 完整 session 录像
- 集成 IAM
附录 S:写作总结的写作总结
SSH 加固的核心思想:
- 把 SSH 当成“重要资产”,不是默认开启的服务
- 多维度纵深防御:网络、协议、账号、审计
- 测试、验证、复盘形成闭环
- 团队 SOP + 监控 + 演练
把这四点刻在脑子里,团队每年演练 1-2 次,把“SSH 加固”变成肌肉记忆。
到这里全篇正文 + 20+ 附录已经覆盖“SSH 暴力破解后的安全加固”。读者可以根据事件类型选用对应章节:
- 急诊断:第 5、附录 A
- 急止血:5.4、附录 G
- 系统加固:5.5 - 5.11、第 7 章
- 长期防御:第 8 章、第 9 章
- 故障复盘:附录 E、第 14 章
整篇文章的核心是一句话:
SSH 暴力破解事件是“组织能力”的试金石:留证、评估、止血、加固、验证、回滚、复盘 —— 任何一个环节缺失,都可能让攻击者乘虚而入。 把这件事做完整,让每一个流程都有回滚、有证据、有总结,团队的安全能力才会真正起来。在 云栈社区,我们经常讨论这类实战问题的解决方案,欢迎各位同行来交流。