SSH安全加固与免密登录实战指南

描述

SSH安全加固与免密登录

一、概述

1.1 背景介绍

线上服务器被暴力破解SSH密码的事每个月都在发生。我们团队去年处理过一起安全事件,一台测试机用了默认22端口加弱密码,48小时内被植入挖矿程序,CPU跑满导致同网段业务受影响。事后复盘发现 /var/log/secure 里有超过20万次失败登录记录,全是字典攻击。

SSH是Linux服务器远程管理的核心通道,OpenSSH默认配置偏向兼容性而非安全性——允许root登录、允许密码认证、监听22端口。这些默认值在公网环境下等于敞开大门。生产环境必须做SSH加固,这不是可选项,是基线要求。

本篇覆盖端口修改、认证方式切换、密钥管理、fail2ban防护、证书认证等完整加固链路,所有配置均在CentOS 7/8/9和Ubuntu 20.04/22.04上线上验证过。

1.2 技术特点

基于非对称加密的身份认证:SSH密钥登录使用公私钥对,私钥不离开客户端,服务端只存公钥。即使服务端被入侵,攻击者拿到的公钥无法反推私钥。ed25519算法密钥长度仅68字节,比RSA-4096的800+字节短得多,签名验证速度快约30%。

支持多种认证方式灵活组合:密码认证、公钥认证、证书认证、GSSAPI认证、键盘交互认证,可以通过 AuthenticationMethods 指令组合使用。比如要求"公钥+密码"双因素,配置为 AuthenticationMethods publickey,password。

细粒度访问控制:通过 AllowUsers、AllowGroups、DenyUsers、DenyGroups 四个指令控制谁能登录,配合 Match 块可以针对特定用户、IP、端口设置不同策略。比如允许运维组从跳板机登录,禁止其他所有来源。

1.3 适用场景

场景一:公网服务器加固。云主机直接暴露在公网,每天承受大量扫描和暴力破解。实测一台新开的阿里云ECS,开机2小时内就有来自全球的SSH登录尝试。必须改端口+禁密码+上fail2ban三件套。

场景二:多人运维团队密钥管理。团队10+人需要登录上百台服务器,用密码管理不现实。通过SSH密钥+跳板机+ProxyJump实现统一入口管理,人员离职时只需在跳板机删除公钥。

场景三:自动化运维免密通道。Ansible、SaltStack等自动化工具依赖SSH免密登录批量执行命令。CI/CD流水线部署也需要SSH免密推送代码到目标机器。密钥认证是自动化的基础。

1.4 环境要求

组件 版本要求 说明
操作系统 CentOS 7+/Ubuntu 20.04+ RHEL系和Debian系均适用,配置路径一致
OpenSSH 7.4+ 7.4开始支持ed25519密钥,8.0+支持证书认证增强
fail2ban 0.10+ 用于防暴力破解,EPEL源或apt直接安装
firewalld/iptables 系统自带 用于限制SSH访问来源IP
Python 3.6+ fail2ban依赖,CentOS 7需手动安装python3

二、详细步骤

2.1 准备工作

2.1.1 系统检查

 

# 检查系统版本
cat /etc/os-release

# 检查当前SSH版本,低于7.4的建议升级
ssh -V

# 检查SSH服务状态
systemctl status sshd

# 检查当前SSH监听端口和连接数
ss -tlnp | grep ssh

# 检查当前登录的SSH会话,确认自己的连接信息
who -u

# 查看最近的SSH登录失败记录,评估当前风险
# CentOS/RHEL
grep "Failed password" /var/log/secure | tail -20

# Ubuntu/Debian
grep "Failed password" /var/log/auth.log | tail -20

 

重要提醒:修改SSH配置前,务必保持当前SSH会话不断开,同时开一个新终端测试。配置改错了当前会话还能用来恢复,断开就只能去机房或用VNC了。我们团队的规矩是改SSH配置必须两人操作,一人改一人保持连接。

2.1.2 安装依赖

 

# CentOS/RHEL 安装
sudo yum install -y epel-release
sudo yum install -y fail2ban fail2ban-systemd openssh-server openssh-clients

# Ubuntu/Debian 安装
sudo apt update
sudo apt install -y fail2ban openssh-server openssh-client

# 确认fail2ban版本
fail2ban-client --version

 

2.1.3 备份原始配置

 

# 备份SSH配置,带日期方便回溯
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d)

# 备份PAM相关SSH配置
sudo cp /etc/pam.d/sshd /etc/pam.d/sshd.bak.$(date +%Y%m%d)

# 验证备份
ls -la /etc/ssh/sshd_config.bak.*

 

2.2 核心配置

2.2.1 修改SSH监听端口

默认22端口是所有扫描器的第一目标。改成高位端口不能防住定向攻击,但能过滤掉99%的自动化扫描。实测改端口后,/var/log/secure 里的失败登录记录从每天几万条降到个位数。

 

# 编辑SSH配置文件
sudo vim /etc/ssh/sshd_config

# 找到 #Port 22,改为:
Port 52222

 

SELinux环境额外操作(CentOS/RHEL默认开启SELinux):

 

# 检查SELinux状态
getenforce

# 如果是Enforcing,需要添加端口到SELinux策略
sudo semanage port -a -t ssh_port_t -p tcp 52222

# 验证端口已添加
sudo semanage port -l | grep ssh
# 输出应包含:ssh_port_t    tcp    52222, 22

 

防火墙放行新端口

 

# firewalld(CentOS 7+)
sudo firewall-cmd --permanent --add-port=52222/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports

# 或者iptables(旧系统)
sudo iptables -A INPUT -p tcp --dport 52222 -j ACCEPT
sudo iptables -D INPUT -p tcp --dport 22 -j ACCEPT
sudo service iptables save

# Ubuntu UFW
sudo ufw allow 52222/tcp
sudo ufw status

 

这个地方有个坑:先放行新端口再改配置重启sshd,顺序反了会把自己锁在外面。

2.2.2 禁用Root直接登录

root账户是暴力破解的首要目标,因为攻击者知道每台Linux都有root用户,只需要猜密码。禁用root登录后,攻击者还得猜用户名,难度指数级上升。

 

# /etc/ssh/sshd_config 中修改
PermitRootLogin no

 

配套操作:确保有一个sudo权限的普通用户可用。

 

# 创建运维用户
sudo useradd -m -s /bin/bash opsadmin
sudo passwd opsadmin

# 加入wheel组(CentOS)或sudo组(Ubuntu)
# CentOS
sudo usermod -aG wheel opsadmin

# Ubuntu
sudo usermod -aG sudo opsadmin

# 验证sudo权限
su - opsadmin
sudo whoami
# 输出应为 root

 

2.2.3 禁用密码认证,仅允许密钥登录

密码认证的问题:再复杂的密码也可能被社工、钓鱼、撞库搞到。密钥认证从原理上杜绝了暴力破解——私钥文件不在网络上传输,服务端只做签名验证。

 

# /etc/ssh/sshd_config 中修改
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
PubkeyAuthentication yes

 

这个参数改错了会导致所有用密码登录的人立刻无法连接,改之前确认密钥登录已经配好并测试通过。我见过不止一次有人先禁密码后配密钥,结果把自己锁在外面。

2.2.4 配置访问白名单

 

# /etc/ssh/sshd_config 中添加
# 只允许特定用户登录
AllowUsers opsadmin deployer monitor

# 或者只允许特定组登录(二选一,不要同时配)
# AllowGroups sshusers ops-team

 

说明:AllowUsers 和 AllowGroups 是白名单机制,配置后不在名单里的用户全部拒绝。如果用 AllowUsers,新增运维人员时记得加到这个列表里,否则加了账号也登不上。我们团队的做法是用 AllowGroups sshusers,新人加入sshusers组就行,不用每次改sshd_config。

 

# 创建SSH用户组
sudo groupadd sshusers

# 将允许登录的用户加入组
sudo usermod -aG sshusers opsadmin
sudo usermod -aG sshusers deployer

 

2.2.5 设置登录超时和重试限制

 

# /etc/ssh/sshd_config 中修改
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
MaxStartups 1060
ClientAliveInterval 300
ClientAliveCountMax 3

 

参数说明

LoginGraceTime 30:用户30秒内必须完成认证,否则断开。默认120秒太长了。

MaxAuthTries 3:单次连接最多尝试3次认证。超过后断开连接,配合fail2ban效果更好。

MaxSessions 5:单个连接最多5个会话复用。

MaxStartups 1060:当未认证连接数达到10个时,以30%概率拒绝新连接;达到60个时100%拒绝。防止连接洪水攻击。

ClientAliveInterval 300:每300秒(5分钟)发一次心跳探测。

ClientAliveCountMax 3:连续3次心跳无响应则断开,即15分钟无响应自动断开。

2.2.6 SSH密钥对生成

 

# 推荐使用ed25519算法
# ed25519比RSA-4096更安全,密钥更短,签名更快
ssh-keygen -t ed25519 -C "opsadmin@company.com" -f ~/.ssh/id_ed25519

# 如果目标系统OpenSSH版本低于6.5,不支持ed25519,退而求其次用RSA-4096
ssh-keygen -t rsa -b 4096 -C "opsadmin@company.com" -f ~/.ssh/id_rsa

# 查看生成的密钥
ls -la ~/.ssh/
# id_ed25519      私钥文件,权限必须是600
# id_ed25519.pub  公钥文件,权限644即可

 

关于密钥密码(passphrase)

生产环境的个人密钥建议设置passphrase,防止私钥文件泄露后被直接使用

自动化场景(Ansible、CI/CD)的密钥不设passphrase,否则每次执行都要输密码

设了passphrase的密钥可以用ssh-agent缓存,避免反复输入

 

# 启动ssh-agent并添加密钥
eval $(ssh-agent -s)
ssh-add ~/.ssh/id_ed25519
# 输入passphrase后,当前会话内不再需要重复输入

# 查看已加载的密钥
ssh-add -l

 

2.2.7 免密登录配置

 

# 方法一:ssh-copy-id(推荐,自动处理权限)
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 52222 opsadmin@192.168.1.100

# 方法二:手动复制(ssh-copy-id不可用时)
cat ~/.ssh/id_ed25519.pub | ssh -p 52222 opsadmin@192.168.1.100 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

# 测试免密登录
ssh -p 52222 opsadmin@192.168.1.100

# 如果登录失败,用verbose模式排查
ssh -vvv -p 52222 opsadmin@192.168.1.100

 

权限要求(这个是最常见的坑):

 

# 服务端权限必须严格设置,多一个权限都不行
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

# 家目录权限不能大于755
chmod 755 ~

# 检查文件属主
ls -la ~/.ssh/
# 所有文件的owner必须是当前用户,不能是root

 

StrictModes yes(默认开启)会检查这些权限,权限不对直接拒绝密钥认证,而且日志里只写 Authentication refused: bad ownership or modes,不告诉你具体哪个文件有问题。

2.2.8 SSH Config配置多主机管理

管理几十上百台服务器时,记IP和端口不现实。SSH Config文件可以给每台机器起别名,配置不同的连接参数。

 

# 编辑客户端配置文件
vim ~/.ssh/config
# 全局默认配置
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes
    Compression yes

# 跳板机
Host jump
    HostName 203.0.113.10
    Port 52222
    User opsadmin
    IdentityFile ~/.ssh/id_ed25519

# 通过跳板机访问内网Web服务器
Host web-prod-01
    HostName 10.0.1.11
    Port 22
    User deployer
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump jump

Host web-prod-02
    HostName 10.0.1.12
    Port 22
    User deployer
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump jump

# 数据库服务器,限制只用特定密钥
Host db-prod-*
    Port 22
    User dbadmin
    IdentityFile ~/.ssh/id_ed25519_db
    ProxyJump jump

Host db-prod-01
    HostName 10.0.2.21

Host db-prod-02
    HostName 10.0.2.22

# 测试环境,直连
Host test-*
    Port 52222
    User opsadmin
    IdentityFile ~/.ssh/id_ed25519

Host test-web-01
    HostName 192.168.100.11

Host test-db-01
    HostName 192.168.100.21
# 配置好后直接用别名连接
ssh web-prod-01
ssh db-prod-01

# scp也能用别名
scp app.jar web-prod-01:/opt/app/

# rsync同样支持
rsync -avz ./dist/ web-prod-01:/var/www/html/

 

说明:IdentitiesOnly yes 这个参数很关键。不加的话ssh会把 ~/.ssh/ 下所有密钥都试一遍,如果密钥多了,试到第3个还没成功就会被 MaxAuthTries 3 拦住,报 Too many authentication failures。加了这个参数后只用指定的密钥文件。

2.2.9 配置fail2ban防暴力破解

fail2ban监控SSH日志,发现短时间内多次登录失败就自动封禁IP。实测效果:部署后暴力破解尝试从每天5万+降到0(因为攻击IP在第5次尝试后就被ban了)。

 

# 创建SSH专用的fail2ban配置
sudo vim /etc/fail2ban/jail.local
[DEFAULT]
# 封禁时间3600秒(1小时),惯犯会递增
bantime = 3600
# 在600秒(10分钟)内
findtime = 600
# 失败5次就封禁
maxretry = 5
# 封禁动作:firewalld或iptables
banaction = firewallcmd-ipset
# 如果用iptables,改为:
# banaction = iptables-multiport

# 忽略的IP(运维跳板机IP,防止把自己封了)
ignoreip = 127.0.0.1/8 10.0.0.0/8 192.168.1.0/24

[sshd]
enabled = true
port = 52222
filter = sshd
logpath = /var/log/secure
# Ubuntu用这个路径:
# logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
findtime = 300
# 启动fail2ban
sudo systemctl start fail2ban
sudo systemctl enable fail2ban

# 查看SSH jail状态
sudo fail2ban-client status sshd

# 输出示例:
# Status for the jail: sshd
# |- Filter
# |  |- Currently failed: 2
# |  |- Total failed:     156
# |  `- File list:        /var/log/secure
# `- Actions
#    |- Currently banned: 3
#    |- Total banned:     47
#    `- Banned IP list:   185.234.xx.xx 103.145.xx.xx 45.148.xx.xx

# 手动解封某个IP(比如同事输错密码被封了)
sudo fail2ban-client set sshd unbanip 192.168.1.50

# 手动封禁某个IP
sudo fail2ban-client set sshd banip 1.2.3.4

 

2.2.10 SSH证书认证(CA签发方式)

密钥认证的问题:每台服务器的 authorized_keys 都要维护,100台服务器就是100份。人员变动时要逐台删除。SSH证书认证用CA统一签发,服务端只信任CA,不需要维护每台机器的authorized_keys。

 

# 1. 生成CA密钥对(在CA服务器上操作,通常是跳板机)
ssh-keygen -t ed25519 -f /etc/ssh/ca_user_key -C "SSH User CA"

# 2. 将CA公钥分发到所有服务器
# 在每台服务器的 /etc/ssh/sshd_config 中添加:
TrustedUserCAKeys /etc/ssh/ca_user_key.pub

# 3. 把CA公钥复制到服务器
sudo scp /etc/ssh/ca_user_key.pub target-server:/etc/ssh/ca_user_key.pub

# 4. 为用户签发证书
# -s 签发者标识
# -I 证书ID(用于审计日志)
# -n 允许登录的用户名列表
# -V 有效期(+52w表示52周)
ssh-keygen -s /etc/ssh/ca_user_key 
    -I "opsadmin-cert-20250101" 
    -n opsadmin,deployer 
    -V +52w 
    /home/opsadmin/.ssh/id_ed25519.pub

# 生成的证书文件:/home/opsadmin/.ssh/id_ed25519-cert.pub

# 5. 查看证书信息
ssh-keygen -L -f /home/opsadmin/.ssh/id_ed25519-cert.pub

# 6. 用户使用证书登录(自动识别,无需额外配置)
ssh -p 52222 opsadmin@192.168.1.100

 

证书吊销

 

# 生成吊销列表
ssh-keygen -k -f /etc/ssh/revoked_keys -s /etc/ssh/ca_user_key /path/to/revoked_cert.pub

# 在sshd_config中配置吊销列表
RevokedKeys /etc/ssh/revoked_keys

# 重载配置
sudo systemctl reload sshd

 

2.3 启动和验证

2.3.1 配置检查和重载

 

# 检查配置文件语法(改完必做,语法错误会导致sshd无法启动)
sudo sshd -t
# 没有输出表示语法正确,有错误会显示具体行号

# 用调试模式检查配置
sudo sshd -T | head -50

# 重载配置(不断开现有连接)
sudo systemctl reload sshd

# 如果reload不生效,再restart(会断开所有连接)
# sudo systemctl restart sshd

# 确认服务状态
sudo systemctl status sshd

 

2.3.2 功能验证

 

# 1. 验证端口变更(新开终端测试,不要断开当前连接)
ssh -p 52222 opsadmin@服务器IP

# 2. 验证root登录已禁用
ssh -p 52222 root@服务器IP
# 预期输出:Permission denied (publickey).

# 3. 验证密码登录已禁用
ssh -p 52222 -o PubkeyAuthentication=no opsadmin@服务器IP
# 预期输出:Permission denied (publickey).

# 4. 验证密钥登录正常
ssh -p 52222 -i ~/.ssh/id_ed25519 opsadmin@服务器IP
# 预期:直接登录成功

# 5. 验证fail2ban工作
# 故意用错误密码尝试几次(在测试环境操作)
sudo fail2ban-client status sshd

# 6. 验证端口监听
ss -tlnp | grep 52222
# 预期输出:LISTEN  0  128  *:52222  *:*  users:(("sshd",pid=xxxx,fd=3))

 

2.3.3 回滚方案

如果加固后出现问题,按以下步骤回滚:

 

# 恢复备份的配置
sudo cp /etc/ssh/sshd_config.bak.$(date +%Y%m%d) /etc/ssh/sshd_config

# 重启SSH服务
sudo systemctl restart sshd

# 如果SSH完全无法连接,通过以下方式恢复:
# 1. 云服务器:通过控制台VNC登录
# 2. 物理机:接显示器键盘直接操作
# 3. 如果有IPMI/iLO/iDRAC远程管理卡,通过带外管理登录

 

三、示例代码和配置

3.1 完整配置示例

3.1.1 生产级sshd_config完整配置

这份配置在我们团队管理的300+台CentOS 7/8和Ubuntu 20.04/22.04服务器上跑了两年多,没出过认证相关的事故。每一行都有注释说明为什么这么配。

 

# 文件路径:/etc/ssh/sshd_config
# 最后修改:2025-01-15
# 说明:生产环境SSH加固配置

# ============================================
# 网络和协议
# ============================================
# 监听端口,改掉默认22
Port 52222

# 只监听IPv4,如果不用IPv6就关掉,减少攻击面
AddressFamily inet

# 绑定特定IP(多网卡服务器建议绑内网IP)
# 如果只允许从内网跳板机连接:
# ListenAddress 10.0.0.100
# 如果需要公网访问:
ListenAddress 0.0.0.0

# 协议版本,只用2(OpenSSH 7.4+已经默认只支持2)
Protocol 2

# ============================================
# 主机密钥
# ============================================
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# 不用DSA和ECDSA
# HostKey /etc/ssh/ssh_host_dsa_key
# HostKey /etc/ssh/ssh_host_ecdsa_key

# ============================================
# 加密算法(只保留安全的算法)
# ============================================
# 密钥交换算法
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

# MAC算法
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

# ============================================
# 认证配置
# ============================================
# 禁止root登录
PermitRootLogin no

# 启用公钥认证
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# 禁用密码认证
PasswordAuthentication no
PermitEmptyPasswords no

# 禁用质询响应认证
ChallengeResponseAuthentication no

# 禁用基于主机的认证
HostbasedAuthentication no
IgnoreRhosts yes

# 禁用GSSAPI(不用Kerberos就关掉,开着会导致连接慢)
GSSAPIAuthentication no
GSSAPICleanupCredentials no

# 禁用X11转发(服务器不需要图形界面)
X11Forwarding no

# 禁用TCP转发(如果不需要SSH隧道)
# AllowTcpForwarding no
# 如果需要SSH隧道做端口转发,保持默认yes
AllowTcpForwarding yes

# 禁用Agent转发(除非明确需要)
AllowAgentForwarding no

# 关闭DNS反向解析(开着会导致连接慢2-5秒)
UseDNS no

# 使用PAM
UsePAM yes

# ============================================
# 访问控制
# ============================================
# 只允许sshusers组的用户登录
AllowGroups sshusers

# 或者指定用户白名单(和AllowGroups二选一)
# AllowUsers opsadmin deployer monitor

# ============================================
# 会话控制
# ============================================
# 认证超时30秒
LoginGraceTime 30

# 最大认证尝试次数
MaxAuthTries 3

# 最大会话数
MaxSessions 5

# 未认证连接限制
MaxStartups 1060

# 客户端存活检测
ClientAliveInterval 300
ClientAliveCountMax 3

# ============================================
# 日志
# ============================================
SyslogFacility AUTH
LogLevel VERBOSE

# ============================================
# 登录Banner
# ============================================
Banner /etc/ssh/banner.txt
PrintMotd no
PrintLastLog yes

# ============================================
# SFTP配置
# ============================================
Subsystem sftp /usr/libexec/openssh/sftp-server -l INFO -f AUTH

# ============================================
# 证书认证(可选)
# ============================================
# TrustedUserCAKeys /etc/ssh/ca_user_key.pub
# RevokedKeys /etc/ssh/revoked_keys

# ============================================
# Match块:针对特定用户/组的特殊配置
# ============================================
# SFTP专用用户,限制在家目录
Match Group sftponly
    ChrootDirectory /data/sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no

# 部署用户,只允许从CI/CD服务器连接
Match User deployer Address 10.0.0.50
    AllowTcpForwarding no
    PermitOpen none

 

登录Banner文件

 

# 文件路径:/etc/ssh/banner.txt
cat > /etc/ssh/banner.txt << 'EOF'
*********************************************************************
*  WARNING: This system is for authorized users only.               *
*  All activities on this system are logged and monitored.          *
*  Unauthorized access will be prosecuted to the full extent of law.*
*********************************************************************
EOF

 

3.1.2 批量分发SSH密钥脚本

管理几十台服务器时,手动一台台ssh-copy-id太慢。这个脚本批量分发公钥,支持密码认证(首次部署时用)和已有密钥认证两种模式。

 

#!/bin/bash
# 文件名:distribute_ssh_keys.sh
# 功能:批量分发SSH公钥到多台服务器
# 依赖:sshpass(首次用密码分发时需要)
# 用法:./distribute_ssh_keys.sh hosts.txt

set -euo pipefail

# ========== 配置区 ==========
SSH_PORT=52222
SSH_USER="opsadmin"
PUB_KEY_FILE="$HOME/.ssh/id_ed25519.pub"
LOG_FILE="/tmp/ssh_key_distribute_$(date +%Y%m%d_%H%M%S).log"
TIMEOUT=10
# =============================

# 颜色输出
RED='�33[0;31m'
GREEN='�33[0;32m'
YELLOW='�33[1;33m'
NC='�33[0m'

log() {
    local level=$1
    shift
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*"
    echo -e "$msg" | tee -a "$LOG_FILE"
}

usage() {
    echo"用法: $0 <主机列表文件>"
    echo""
    echo"主机列表文件格式(每行一个IP):"
    echo"192.168.1.101"
    echo"192.168.1.102"
    echo"10.0.1.11"
    exit 1
}

# 参数检查
if [[ $# -ne 1 ]]; then
    usage
fi

HOST_FILE=$1

if [[ ! -f "$HOST_FILE" ]]; then
    log"ERROR""主机列表文件不存在: $HOST_FILE"
    exit 1
fi

if [[ ! -f "$PUB_KEY_FILE" ]]; then
    log"ERROR""公钥文件不存在: $PUB_KEY_FILE"
    log"INFO""请先生成密钥: ssh-keygen -t ed25519"
    exit 1
fi

# 检查sshpass是否安装
USE_PASSWORD=false
ifcommand -v sshpass &>/dev/null; then
    read -sp "输入SSH密码(如果目标机器已配置密钥登录,直接回车跳过): " SSH_PASS
    echo
    if [[ -n "$SSH_PASS" ]]; then
        USE_PASSWORD=true
    fi
fi

# 统计
TOTAL=0
SUCCESS=0
FAILED=0

log"INFO""开始分发SSH公钥"
log"INFO""公钥文件: $PUB_KEY_FILE"
log"INFO""目标用户: $SSH_USER"
log"INFO""SSH端口: $SSH_PORT"

while IFS= read -r host; do
    # 跳过空行和注释
    [[ -z "$host" || "$host" =~ ^# ]] && continue

    TOTAL=$((TOTAL + 1))
    log"INFO""[$TOTAL] 正在处理: $host"

    if$USE_PASSWORD; then
        # 使用密码分发
        if sshpass -p "$SSH_PASS" ssh-copy-id 
            -i "$PUB_KEY_FILE" 
            -p "$SSH_PORT" 
            -o StrictHostKeyChecking=no 
            -o ConnectTimeout=$TIMEOUT 
            "${SSH_USER}@${host}" 2>>"$LOG_FILE"; then
            log"INFO""${GREEN}成功${NC}: $host"
            SUCCESS=$((SUCCESS + 1))
        else
            log"ERROR""${RED}失败${NC}: $host"
            FAILED=$((FAILED + 1))
        fi
    else
        # 使用已有密钥分发新密钥
        if ssh-copy-id 
            -i "$PUB_KEY_FILE" 
            -p "$SSH_PORT" 
            -o StrictHostKeyChecking=no 
            -o ConnectTimeout=$TIMEOUT 
            "${SSH_USER}@${host}" 2>>"$LOG_FILE"; then
            log"INFO""${GREEN}成功${NC}: $host"
            SUCCESS=$((SUCCESS + 1))
        else
            log"ERROR""${RED}失败${NC}: $host"
            FAILED=$((FAILED + 1))
        fi
    fi
done < "$HOST_FILE"

log"INFO""========== 分发完成 =========="
log"INFO""总计: $TOTAL  成功: $SUCCESS  失败: $FAILED"
log"INFO""详细日志: $LOG_FILE"

if [[ $FAILED -gt 0 ]]; then
    log"WARN""有 $FAILED 台服务器分发失败,请检查日志"
    exit 1
fi
# 使用方法
chmod +x distribute_ssh_keys.sh

# 准备主机列表
cat > hosts.txt << 'EOF'
192.168.1.101
192.168.1.102
192.168.1.103
10.0.1.11
10.0.1.12
EOF

# 执行分发
./distribute_ssh_keys.sh hosts.txt

 

3.2 实际应用案例

案例一:基于跳板机的SSH ProxyJump多层跳转

场景描述:生产环境网络架构分三层——公网跳板机、DMZ区应用服务器、内网数据库服务器。运维人员从办公网络连接跳板机,再跳转到内网服务器。数据库服务器只允许从应用服务器网段访问。

网络拓扑

 

办公网络(172.16.0.0/16)
    |
    v
跳板机(公网: 203.0.113.10, 内网: 10.0.0.1)
    |
    v
应用服务器(10.0.1.0/24)
    |
    v
数据库服务器(10.0.2.0/24)

 

SSH Config配置

 

# ~/.ssh/config

# 跳板机(一跳)
Host jump
    HostName 203.0.113.10
    Port 52222
    User opsadmin
    IdentityFile ~/.ssh/id_ed25519
    # 跳板机上开启Agent转发,用于二次跳转
    ForwardAgent yes

# 应用服务器(二跳,通过跳板机)
Host app-01
    HostName 10.0.1.11
    User deployer
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump jump

Host app-02
    HostName 10.0.1.12
    User deployer
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump jump

# 数据库服务器(三跳,通过应用服务器)
Host db-master
    HostName 10.0.2.21
    User dbadmin
    IdentityFile ~/.ssh/id_ed25519_db
    ProxyJump app-01

Host db-slave
    HostName 10.0.2.22
    User dbadmin
    IdentityFile ~/.ssh/id_ed25519_db
    ProxyJump app-01

 

使用效果

 

# 直接连接数据库服务器,SSH自动完成两次跳转
ssh db-master
# 实际路径:本机 -> jump(203.0.113.10) -> app-01(10.0.1.11) -> db-master(10.0.2.21)

# 通过跳板机做端口转发,本地访问远程数据库
ssh -L 33073306 app-01
# 然后本地用 mysql -h 127.0.0.1 -P 3307 连接

# 通过跳板机传文件到内网服务器
scp backup.sql db-master:/tmp/

 

案例二:多环境SSH Config管理与自动切换

场景描述:团队管理开发、测试、预发布、生产四套环境,共200+台服务器。不同环境用不同的密钥和用户,需要一套清晰的管理方案。

目录结构

 

~/.ssh/
├── config                  # 主配置文件,include其他配置
├── config.d/
│   ├── 00-defaults.conf    # 全局默认配置
│   ├── 10-dev.conf         # 开发环境
│   ├── 20-test.conf        # 测试环境
│   ├── 30-staging.conf     # 预发布环境
│   └── 40-prod.conf        # 生产环境
├── id_ed25519              # 默认密钥
├── id_ed25519_prod         # 生产环境专用密钥
├── id_ed25519_db           # 数据库专用密钥
└── known_hosts

 

主配置文件

 

# ~/.ssh/config
# 使用Include指令加载分环境配置(OpenSSH 7.3+支持)
Include config.d/*.conf

 

全局默认配置

 

# ~/.ssh/config.d/00-defaults.conf
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes
    Compression yes
    # 连接复用,同一台服务器的多个SSH会话共用一个TCP连接
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600
    # 首次连接自动接受主机密钥(仅限内网环境,公网建议去掉)
    # StrictHostKeyChecking accept-new

 

生产环境配置

 

# ~/.ssh/config.d/40-prod.conf
# 生产环境 - 通过跳板机访问
Host prod-jump
    HostName 203.0.113.10
    Port 52222
    User opsadmin
    IdentityFile ~/.ssh/id_ed25519_prod

Host prod-web-*
    User deployer
    IdentityFile ~/.ssh/id_ed25519_prod
    ProxyJump prod-jump

Host prod-web-01
    HostName 10.0.1.11
Host prod-web-02
    HostName 10.0.1.12
Host prod-web-03
    HostName 10.0.1.13

Host prod-db-*
    User dbadmin
    IdentityFile ~/.ssh/id_ed25519_db
    ProxyJump prod-jump

Host prod-db-master
    HostName 10.0.2.21
Host prod-db-slave-01
    HostName 10.0.2.22
Host prod-db-slave-02
    HostName 10.0.2.23
# 创建socket目录(连接复用需要)
mkdir -p ~/.ssh/sockets

# 使用效果
ssh prod-web-01      # 连接生产Web服务器
ssh prod-db-master   # 连接生产数据库主库

# 查看当前活跃的连接复用
ls ~/.ssh/sockets/

# 手动关闭某个复用连接
ssh -O exit prod-web-01

 

案例三:SSH密钥自动轮换脚本

场景描述:安全合规要求SSH密钥每90天轮换一次。手动操作容易遗漏,写个脚本自动化处理。

 

#!/bin/bash
# 文件名:rotate_ssh_keys.sh
# 功能:自动轮换SSH密钥并分发到目标服务器
# 建议配合crontab每季度执行一次

set -euo pipefail

KEY_DIR="$HOME/.ssh"
KEY_TYPE="ed25519"
KEY_COMMENT="$(whoami)@$(hostname)-$(date +%Y%m%d)"
BACKUP_DIR="$KEY_DIR/archived_keys"
HOST_FILE="$HOME/.ssh/managed_hosts.txt"
LOG_FILE="/var/log/ssh_key_rotation_$(date +%Y%m%d).log"

log() {
    echo"[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 1. 备份旧密钥
if [[ -f "$KEY_DIR/id_${KEY_TYPE}" ]]; then
    BACKUP_NAME="id_${KEY_TYPE}_$(date +%Y%m%d_%H%M%S)"
    cp "$KEY_DIR/id_${KEY_TYPE}""$BACKUP_DIR/$BACKUP_NAME"
    cp "$KEY_DIR/id_${KEY_TYPE}.pub""$BACKUP_DIR/${BACKUP_NAME}.pub"
    log"旧密钥已备份到: $BACKUP_DIR/$BACKUP_NAME"
fi

# 2. 生成新密钥(不设passphrase,自动化场景用)
ssh-keygen -t "$KEY_TYPE" -C "$KEY_COMMENT" -f "$KEY_DIR/id_${KEY_TYPE}" -N "" -q
log"新密钥已生成: $KEY_DIR/id_${KEY_TYPE}"

# 3. 分发新公钥到所有服务器(用旧密钥认证)
if [[ -f "$HOST_FILE" ]]; then
    while IFS=: read -r host port user; do
        port=${port:-52222}
        user=${user:-opsadmin}
        log"分发到: ${user}@${host}:${port}"
        if ssh-copy-id -i "$KEY_DIR/id_${KEY_TYPE}.pub" 
            -p "$port" 
            -o ConnectTimeout=10 
            -o IdentityFile="$BACKUP_DIR/$(ls -t $BACKUP_DIR/id_${KEY_TYPE}_* 2>/dev/null | head -1)" 
            "${user}@${host}" 2>>"$LOG_FILE"; then
            log"成功: ${host}"
        else
            log"失败: ${host} - 需要手动处理"
        fi
    done < "$HOST_FILE"
fi

# 4. 验证新密钥可用
log"验证新密钥..."
if [[ -f "$HOST_FILE" ]]; then
    while IFS=: read -r host port user; do
        port=${port:-52222}
        user=${user:-opsadmin}
        if ssh -p "$port" -o ConnectTimeout=5 -o BatchMode=yes 
            "${user}@${host}""echo ok" &>/dev/null; then
            log"验证通过: ${host}"
        else
            log"验证失败: ${host} - 紧急!请检查"
        fi
    done < "$HOST_FILE"
fi

log"密钥轮换完成"
# managed_hosts.txt 格式:主机:端口:用户
cat > ~/.ssh/managed_hosts.txt << 'EOF'
192.168.1.101opsadmin
192.168.1.102opsadmin
10.0.1.11deployer
10.0.1.12deployer
EOF

 

四、最佳实践和注意事项

4.1 最佳实践

4.1.1 性能优化

使用ed25519替代RSA密钥:ed25519密钥长度只有68字节,RSA-4096是800+字节。实测签名速度ed25519比RSA-4096快约30%,在批量SSH操作(Ansible管理500台机器)时差异明显。Ansible playbook跑完全量主机,ed25519密钥比RSA-4096快了约12秒(总耗时从98秒降到86秒)。

 

# 生成ed25519密钥
ssh-keygen -t ed25519 -C "ops@company.com"

# 如果已有RSA密钥,生成新的ed25519密钥后逐步替换
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "ops@company.com"

 

开启连接复用(ControlMaster):同一台服务器的多个SSH会话共用一个TCP连接,省去重复的TCP握手和密钥交换。实测第二次连接耗时从1.2秒降到0.1秒。对于频繁ssh/scp操作的场景提升巨大。

 

# ~/.ssh/config 中配置
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

# 创建socket目录
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets

 

关闭DNS反向解析和GSSAPI:sshd默认会对客户端IP做DNS反向解析,如果DNS服务器响应慢或不可达,每次连接会卡5-30秒。GSSAPI认证同理,不用Kerberos就关掉。

 

# 服务端 /etc/ssh/sshd_config
UseDNS no
GSSAPIAuthentication no

# 客户端 ~/.ssh/config(双向都关)
Host *
    GSSAPIAuthentication no

 

启用压缩传输:在带宽有限的网络环境下(比如跨地域机房),开启压缩可以减少传输数据量。实测传输日志文件(文本类数据压缩率高)速度提升40-60%。但在局域网高带宽环境下,压缩反而增加CPU开销,建议关闭。

 

# 低带宽环境开启
Host slow-network-*
    Compression yes

# 局域网环境关闭
Host lan-*
    Compression no

 

4.1.2 安全加固

限制SSH访问来源IP:即使改了端口、禁了密码,也建议在防火墙层面限制只允许特定IP段访问SSH端口。纵深防御,多一层保护。

 

# firewalld:只允许办公网络和跳板机IP访问SSH
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --permanent --new-zone=ssh-restricted 2>/dev/null || true
sudo firewall-cmd --permanent --zone=ssh-restricted --add-source=172.16.0.0/16
sudo firewall-cmd --permanent --zone=ssh-restricted --add-source=203.0.113.10/32
sudo firewall-cmd --permanent --zone=ssh-restricted --add-port=52222/tcp
sudo firewall-cmd --reload

# iptables方式
sudo iptables -A INPUT -p tcp --dport 52222 -s 172.16.0.0/16 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 52222 -s 203.0.113.10/32 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 52222 -j DROP
sudo service iptables save

 

定期审计SSH登录日志:每周检查一次异常登录记录,关注非工作时间登录、陌生IP登录、频繁失败尝试。

 

# 查看成功登录记录
grep "Accepted" /var/log/secure | awk '{print $1,$2,$3,$9,$11}' | sort | uniq -c | sort -rn | head -20

# 查看失败登录统计(按IP排序)
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

# 查看非工作时间(2200)的登录
grep "Accepted" /var/log/secure | awk '{split($3,t,":"); if(t[1]>=22 || t[1]<8) print}'

 

SSH密钥指纹验证:首次连接新服务器时,SSH会提示确认主机指纹。生产环境不要无脑yes,应该提前通过安全渠道获取服务器指纹并核对。

 

# 在服务器上查看主机密钥指纹
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

# 客户端连接时核对指纹
# The authenticity of host '192.168.1.100 (192.168.1.100)' can't be established.
# ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
# 核对一致后输入yes

 

禁用弱加密算法:默认配置包含一些老旧的加密算法(如arcfour、3des-cbc),存在已知漏洞。只保留安全的算法。

 

# /etc/ssh/sshd_config
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

# 验证当前使用的算法
ssh -vv -p 52222 opsadmin@192.168.1.100 2>&1 | grep "kex:"

 

4.1.3 高可用配置

跳板机高可用:跳板机是单点,挂了所有人都连不上内网服务器。生产环境至少部署两台跳板机,用DNS轮询或keepalived做VIP漂移。

 

# SSH Config中配置备用跳板机
Host jump
    HostName jump-vip.company.com
    Port 52222
    User opsadmin
    IdentityFile ~/.ssh/id_ed25519
    # 连接超时后自动尝试备用
    ConnectTimeout 5

# 或者用Match块配置fallback
# 主跳板机
Host jump-primary
    HostName 203.0.113.10
    Port 52222

# 备用跳板机
Host jump-backup
    HostName 203.0.113.11
    Port 52222

 

SSH服务端口探活:用监控系统定期检测SSH端口是否可达,sshd进程是否存活。

 

# 简单的SSH端口探活脚本
nc -z -w 3 192.168.1.100 52222 && echo "SSH OK" || echo "SSH DOWN"

# 或者用ssh命令探活(更准确,验证到协议层)
ssh -o ConnectTimeout=3 -o BatchMode=yes -p 52222 opsadmin@192.168.1.100 "echo ok" 2>/dev/null

 

备份策略:SSH配置文件和密钥是关键资产,必须纳入备份。

/etc/ssh/sshd_config 和 /etc/ssh/ssh_host_* 主机密钥:纳入系统配置备份

用户 ~/.ssh/ 目录:纳入用户数据备份

CA密钥(如果用证书认证):离线备份,存放在保险柜级别的安全位置

4.2 注意事项

4.2.1 配置注意事项

改SSH配置前必须保持一个活跃会话不断开。 这是铁律,违反一次就可能要跑机房。我亲眼见过同事改错sshd_config后restart,所有SSH连接断开,最后开车去机房用显示器键盘恢复的。

修改sshd_config后先用 sshd -t 检查语法,再reload而不是restart

改端口时先在防火墙放行新端口,再改配置重启,顺序不能反

禁用密码认证前,必须确认密钥登录已经配好并测试通过

AllowUsers 和 AllowGroups 是白名单,配了之后不在名单里的用户全部被拒绝,包括root

Match 块必须放在sshd_config文件末尾,Match块之后的配置都属于这个Match块的作用域

4.2.2 常见错误

错误现象 原因分析 解决方案
Permission denied (publickey) 公钥未添加到authorized_keys,或文件权限不对 检查~/.ssh/目录700、authorized_keys文件600、家目录不超过755
Connection refused sshd未启动或端口不对 systemctl status sshd  检查服务状态,ss -tlnp 检查端口
Connection timed out 防火墙未放行端口或网络不通 telnet IP PORT  测试端口连通性,检查firewalld/iptables规则
Too many authentication failures 客户端尝试了太多密钥,超过MaxAuthTries 在~/.ssh/config中加 IdentitiesOnly yes 指定密钥
SSH连接后卡住5-10秒 DNS反向解析超时或GSSAPI认证超时 服务端设 UseDNS no,客户端设 GSSAPIAuthentication no
Host key verification failed 服务器重装后主机密钥变了 ssh-keygen -R 主机IP  删除旧指纹,重新确认
Bad owner or modes .ssh目录或文件权限过大 chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys

4.2.3 兼容性问题

版本兼容:ed25519密钥需要OpenSSH 6.5+,ProxyJump指令需要7.3+,Include指令需要7.3+。CentOS 6自带的OpenSSH 5.3不支持这些特性,需要升级或用ProxyCommand替代ProxyJump。

# CentOS 6上用ProxyCommand替代ProxyJump
Host internal-server
    HostName 10.0.1.11
    ProxyCommand ssh -W %h:%p jump

 

平台兼容:macOS自带的OpenSSH版本通常较新(Ventura自带8.6),但ssh-copy-id需要额外安装(brew install ssh-copy-id)。Windows 10/11自带OpenSSH客户端,但版本可能较旧,建议用Git Bash或WSL。

组件依赖:fail2ban在CentOS 7上依赖EPEL源;CentOS 8/9的fail2ban需要python3-systemd包;Ubuntu直接apt安装即可。semanage命令需要安装policycoreutils-python-utils包。

五、故障排查和监控

5.1 故障排查

5.1.1 日志查看

 

# CentOS/RHEL 查看SSH认证日志
sudo tail -f /var/log/secure

# Ubuntu/Debian 查看SSH认证日志
sudo tail -f /var/log/auth.log

# 用journalctl查看sshd日志(systemd系统通用)
sudo journalctl -u sshd -f

# 只看最近1小时的SSH日志
sudo journalctl -u sshd --since "1 hour ago"

# 过滤失败登录
sudo journalctl -u sshd | grep -i "failed|error|denied"

# 查看fail2ban日志
sudo tail -f /var/log/fail2ban.log

 

日志级别调整:排查问题时临时调高日志级别,排查完改回来。

 

# /etc/ssh/sshd_config
# 正常运行用VERBOSE,排查问题临时改为DEBUG3
LogLevel VERBOSE
# LogLevel DEBUG3

# 改完reload
sudo systemctl reload sshd

 

DEBUG3级别会记录每一步认证细节,包括尝试了哪些密钥、为什么拒绝等。日志量很大,排查完务必改回VERBOSE,否则磁盘会被撑满。

5.1.2 常见问题排查

问题一:Permission denied (publickey) —— 密钥认证失败

这是最常见的SSH问题,原因有很多种,按排查优先级列出:

 

# 1. 客户端用verbose模式连接,看具体卡在哪一步
ssh -vvv -p 52222 -i ~/.ssh/id_ed25519 opsadmin@192.168.1.100

# 关注这些关键行:
# "Offering public key: /home/user/.ssh/id_ed25519 ED25519" -> 客户端发送了密钥
# "Server accepts key: /home/user/.ssh/id_ed25519 ED25519" -> 服务端接受了密钥
# "Authentication succeeded (publickey)" -> 认证成功

# 如果看到 "No more authentication methods to try" 说明服务端拒绝了所有密钥
# 2. 检查服务端权限(最常见的原因)
ls -la ~/
# 家目录权限不能大于755,如果是777就会被拒绝
ls -la ~/.ssh/
# .ssh目录必须是700
ls -la ~/.ssh/authorized_keys
# authorized_keys必须是600

# 修复权限
chmod 755 ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# 3. 检查authorized_keys内容
cat ~/.ssh/authorized_keys
# 确认公钥完整,没有换行符截断
# 每个公钥必须是一行,不能有折行

# 4. 检查文件属主
ls -la ~/.ssh/authorized_keys
# owner必须是当前用户,不能是root
chown $(whoami):$(whoami) ~/.ssh/authorized_keys

# 5. 检查SELinux上下文(CentOS/RHEL)
ls -Z ~/.ssh/authorized_keys
# 应该是 unconfined_ussh_home_t:s0
# 如果不对,恢复上下文:
restorecon -Rv ~/.ssh/

 

问题二:SSH连接慢,登录要等5-30秒

 

# 原因1:DNS反向解析(最常见)
# 服务端对客户端IP做反向DNS查询,DNS服务器不可达时会等到超时
# 解决:
sudo grep "UseDNS" /etc/ssh/sshd_config
# 如果是yes或者没配(默认yes),改为no
# UseDNS no

# 原因2:GSSAPI认证超时
# 客户端尝试GSSAPI认证,没有Kerberos环境时会超时
# 解决(客户端):
ssh -o GSSAPIAuthentication=no -p 52222 opsadmin@192.168.1.100
# 或者在~/.ssh/config中全局关闭
# Host *
#     GSSAPIAuthentication no

# 原因3:systemd-logind响应慢
# CentOS 7上偶发,dbus通信超时
# 诊断:
sudo journalctl -u systemd-logind --since "10 minutes ago"
# 解决:
sudo systemctl restart systemd-logind

# 用time命令量化连接耗时
time ssh -p 52222 opsadmin@192.168.1.100 "exit"
# 正常应该在1秒以内

 

问题三:Connection refused —— 连接被拒绝

 

# 1. 检查sshd是否在运行
sudo systemctl status sshd
# 如果是dead/failed状态,查看原因
sudo journalctl -u sshd --no-pager | tail -30

# 2. 检查监听端口
ss -tlnp | grep sshd
# 确认sshd在监听正确的端口

# 3. 检查配置文件语法
sudo sshd -t
# 如果有语法错误,sshd可能启动失败

# 4. 检查防火墙
sudo firewall-cmd --list-all
# 或
sudo iptables -L -n | grep 52222

# 5. 检查SELinux是否阻止了非标准端口
sudo semanage port -l | grep ssh
# 如果新端口不在列表里:
sudo semanage port -a -t ssh_port_t -p tcp 52222

# 6. 检查TCP Wrappers(/etc/hosts.allow 和 /etc/hosts.deny)
cat /etc/hosts.deny
# 如果有 sshd: ALL 会拒绝所有SSH连接

 

问题四:fail2ban误封了合法IP

 

# 查看当前被封禁的IP列表
sudo fail2ban-client status sshd

# 解封特定IP
sudo fail2ban-client set sshd unbanip 172.16.1.50

# 查看封禁原因(在fail2ban日志中搜索)
sudo grep "172.16.1.50" /var/log/fail2ban.log

# 将合法IP加入白名单(永久生效)
# 编辑 /etc/fail2ban/jail.local
# ignoreip = 127.0.0.1/8 10.0.0.0/8 172.16.0.0/16

# 重启fail2ban使白名单生效
sudo systemctl restart fail2ban

 

5.1.3 调试模式

 

# 在前台以调试模式启动sshd(不影响正在运行的sshd)
# 监听在不同端口避免冲突
sudo /usr/sbin/sshd -d -p 52223

# 客户端连接调试端口
ssh -vvv -p 52223 opsadmin@192.168.1.100

# 两边的输出对照看,能精确定位认证失败的原因

# 检查sshd加载的完整配置(排查配置覆盖问题)
sudo sshd -T

# 检查特定用户从特定IP连接时的有效配置(Match块生效情况)
sudo sshd -T -C user=deployer,host=10.0.0.50,addr=10.0.0.50

 

5.2 性能监控

5.2.1 关键指标监控

 

# SSH连接数监控
ss -tnp | grep ":52222" | wc -l

# 当前活跃SSH会话数
who | wc -l

# sshd进程资源占用
ps aux | grep sshd | grep -v grep

# SSH认证失败频率(最近1小时)
sudo journalctl -u sshd --since "1 hour ago" | grep -c "Failed"

# fail2ban封禁统计
sudo fail2ban-client status sshd | grep "Currently banned"

# SSH端口连接状态分布
ss -tn | grep ":52222" | awk '{print $1}' | sort | uniq -c

 

5.2.2 监控指标说明

指标名称 正常范围 告警阈值 说明
SSH活跃连接数 0-50 >100 超过100可能是暴力破解或连接泄漏
认证失败次数/小时 0-10 >50 大量失败说明有暴力破解行为
fail2ban封禁IP数 0-5 >20 大量封禁说明正在遭受攻击
SSH连接延迟 <1s >5s 延迟高需要排查DNS/GSSAPI/网络问题
sshd进程CPU使用率 <5% >30% CPU高可能是密钥交换风暴或DDoS
sshd进程内存使用 <50MB >200MB 内存异常增长需要排查连接泄漏

5.2.3 Prometheus监控规则

 

# prometheus_ssh_rules.yml
# 文件路径:/etc/prometheus/rules/ssh_rules.yml

groups:
-name:ssh_security
    interval:30s
    rules:
      # SSH认证失败率告警
      -alert:SSHAuthFailureHigh
        expr:rate(ssh_auth_failures_total[5m])>10
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"SSH认证失败率过高 ({{ $labels.instance }})"
          description:"5分钟内SSH认证失败率超过10次/秒,可能遭受暴力破解"

      # SSH活跃连接数告警
      -alert:SSHConnectionsHigh
        expr:ssh_active_connections>100
        for:2m
        labels:
          severity:warning
        annotations:
          summary:"SSH连接数过高 ({{ $labels.instance }})"
          description:"SSH活跃连接数 {{ $value }},超过阈值100"

      # fail2ban封禁数告警
      -alert:Fail2banBannedIPsHigh
        expr:fail2ban_banned_ips{jail="sshd"}>20
        for:5m
        labels:
          severity:critical
        annotations:
          summary:"fail2ban封禁IP数过多 ({{ $labels.instance }})"
          description:"SSH jail当前封禁 {{ $value }} 个IP,可能正在遭受大规模攻击"

      # sshd进程存活检测
      -alert:SSHDProcessDown
        expr:node_systemd_unit_state{name="sshd.service",state="active"}!=1
        for:1m
        labels:
          severity:critical
        annotations:
          summary:"sshd服务异常 ({{ $labels.instance }})"
          description:"sshd服务未在运行状态,远程管理通道中断"

 

配合node_exporter的textfile collector采集SSH指标

 

#!/bin/bash
# 文件名:ssh_metrics.sh
# 功能:采集SSH相关指标,输出为Prometheus格式
# 配合crontab每分钟执行:* * * * * /opt/scripts/ssh_metrics.sh

METRICS_DIR="/var/lib/node_exporter/textfile_collector"
METRICS_FILE="$METRICS_DIR/ssh_metrics.prom"

mkdir -p "$METRICS_DIR"

# 活跃SSH连接数
ACTIVE_CONN=$(ss -tnp | grep -c ":52222" 2>/dev/null || echo 0)

# 当前登录用户数
LOGGED_USERS=$(who | wc -l)

# 最近5分钟认证失败次数
FAIL_COUNT=$(sudo journalctl -u sshd --since "5 minutes ago" 2>/dev/null | grep -c "Failed" || echo 0)

# fail2ban封禁IP数
BANNED_IPS=$(sudo fail2ban-client status sshd 2>/dev/null | grep "Currently banned" | awk '{print $NF}' || echo 0)

cat > "$METRICS_FILE.tmp" << EOF
# HELP ssh_active_connections Current number of SSH connections
# TYPE ssh_active_connections gauge
ssh_active_connections $ACTIVE_CONN
# HELP ssh_logged_users Current number of logged in users
# TYPE ssh_logged_users gauge
ssh_logged_users $LOGGED_USERS
# HELP ssh_auth_failures_5m SSH authentication failures in last 5 minutes
# TYPE ssh_auth_failures_5m gauge
ssh_auth_failures_5m $FAIL_COUNT
# HELP fail2ban_banned_ips Number of IPs banned by fail2ban
# TYPE fail2ban_banned_ips gauge
fail2ban_banned_ips{jail="sshd"} $BANNED_IPS
EOF

mv "$METRICS_FILE.tmp""$METRICS_FILE"

 

5.3 备份与恢复

5.3.1 备份策略

 

#!/bin/bash
# 文件名:backup_ssh_config.sh
# 功能:备份SSH服务端和客户端配置
# 建议每周执行一次,保留最近12周的备份

BACKUP_BASE="/data/backup/ssh"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_BASE/$DATE"
KEEP_WEEKS=12

mkdir -p "$BACKUP_DIR"

# 备份服务端配置
sudo cp -a /etc/ssh/sshd_config "$BACKUP_DIR/"
sudo cp -a /etc/ssh/ssh_config "$BACKUP_DIR/" 2>/dev/null
sudo cp -a /etc/ssh/banner.txt "$BACKUP_DIR/" 2>/dev/null

# 备份主机密钥(恢复时需要,否则所有客户端会报host key changed)
sudo cp -a /etc/ssh/ssh_host_* "$BACKUP_DIR/"

# 备份fail2ban配置
sudo cp -a /etc/fail2ban/jail.local "$BACKUP_DIR/" 2>/dev/null

# 备份CA密钥(如果有)
sudo cp -a /etc/ssh/ca_user_key* "$BACKUP_DIR/" 2>/dev/null
sudo cp -a /etc/ssh/revoked_keys "$BACKUP_DIR/" 2>/dev/null

# 设置备份文件权限
sudo chmod 600 "$BACKUP_DIR"/ssh_host_*
sudo chmod 600 "$BACKUP_DIR"/ca_user_key 2>/dev/null

# 清理过期备份
find "$BACKUP_BASE" -maxdepth 1 -type d -mtime +$((KEEP_WEEKS * 7)) -exec rm -rf {} ;

echo"SSH配置备份完成: $BACKUP_DIR"
ls -la "$BACKUP_DIR/"

 

5.3.2 恢复流程

停止服务(如果sshd还在运行):

 

# 不要直接stop,先确认有其他方式访问服务器(VNC/IPMI/控制台)
sudo systemctl stop sshd

 

恢复配置文件

 

# 找到最近的备份
ls -lt /data/backup/ssh/ | head -5

# 恢复配置
RESTORE_DIR="/data/backup/ssh/20250115_020000"
sudo cp "$RESTORE_DIR/sshd_config" /etc/ssh/sshd_config
sudo cp "$RESTORE_DIR"/ssh_host_* /etc/ssh/

# 恢复权限
sudo chmod 600 /etc/ssh/ssh_host_*_key
sudo chmod 644 /etc/ssh/ssh_host_*_key.pub
sudo chmod 644 /etc/ssh/sshd_config

 

验证配置

 

sudo sshd -t

 

重启服务

 

sudo systemctl start sshd
sudo systemctl status sshd
ss -tlnp | grep sshd

 

六、总结

6.1 技术要点回顾

端口+认证双重加固:改默认端口过滤自动化扫描,禁密码认证杜绝暴力破解。实测改端口后扫描日志从每天数万条降到个位数,禁密码后暴力破解彻底归零。

ed25519是当前最优密钥算法:比RSA-4096更安全、密钥更短(68字节 vs 800+字节)、签名验证更快(约30%)。除非目标系统OpenSSH低于6.5,否则一律用ed25519。

fail2ban是必备防护组件:配合MaxAuthTries形成两层防线——MaxAuthTries限制单次连接尝试次数,fail2ban在多次连接失败后封禁IP。两者配合效果远大于单独使用。

SSH Config + ProxyJump实现高效多主机管理:用别名替代IP+端口,用ProxyJump实现透明跳转,用ControlMaster实现连接复用。管理200+台服务器和管理2台一样方便。

证书认证是大规模环境的终极方案:超过50台服务器时,逐台维护authorized_keys不现实。CA签发证书后服务端只需信任CA公钥,人员变动只需吊销证书,不用逐台操作。

配置变更必须有回滚方案:改SSH配置前备份、保持活跃会话、先放行新端口再改配置、用sshd -t检查语法。这些流程每一步都不能省。

6.2 进阶学习方向

SSH证书认证与HashiCorp Vault集成

Vault可以作为SSH CA,动态签发短期证书(比如有效期8小时),实现"用完即废"的零信任模式

学习资源:HashiCorp Vault官方文档 SSH Secrets Engine章节

实践建议:先在测试环境搭建Vault,配置SSH Secrets Engine,体验动态证书签发流程

基于FIDO2/U2F硬件密钥的SSH认证

OpenSSH 8.2+支持FIDO2安全密钥(如YubiKey),私钥存储在硬件中无法导出,比软件密钥更安全

学习资源:OpenSSH 8.2 Release Notes,Yubico官方SSH配置指南

实践建议:购买一个YubiKey 5系列,配置 ssh-keygen -t ed25519-sk 生成硬件绑定密钥

Teleport/Boundary等零信任SSH网关

替代传统跳板机,提供会话录制、RBAC权限控制、审计日志、MFA集成等企业级功能

学习资源:Teleport官方文档,Gravitational GitHub仓库

实践建议:用Docker快速部署Teleport试用版,体验Web Terminal和会话回放功能

6.3 参考资料

OpenSSH官方文档 - sshd_config所有参数的权威说明

Mozilla SSH安全指南 - Mozilla内部SSH加固标准,推荐的加密算法列表

fail2ban官方Wiki - fail2ban配置详解和自定义filter编写

SSH Mastery (Michael W Lucas) - SSH进阶书籍,覆盖证书认证、端口转发等高级主题

CIS Benchmark for Linux - CIS安全基线中SSH加固章节

附录

A. 命令速查表

 

# ===== 密钥管理 =====
ssh-keygen -t ed25519 -C "comment"              # 生成ed25519密钥
ssh-keygen -t rsa -b 4096 -C "comment"           # 生成RSA-4096密钥
ssh-keygen -lf ~/.ssh/id_ed25519.pub              # 查看密钥指纹
ssh-keygen -R 192.168.1.100                       # 删除known_hosts中的主机记录
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 52222 user@host  # 分发公钥

# ===== 连接和调试 =====
ssh -p 52222 user@host                            # 指定端口连接
ssh -vvv user@host                                # 详细调试模式
ssh -J jump user@internal-host                    # 通过跳板机连接
ssh -L 33073306 user@jump                # 本地端口转发
ssh -D 1080 user@host                             # SOCKS代理

# ===== 服务管理 =====
sudo sshd -t                                      # 检查配置语法
sudo sshd -T                                      # 显示完整有效配置
sudo systemctl reload sshd                        # 重载配置(不断连接)
sudo systemctl restart sshd                       # 重启服务(断开所有连接)

# ===== fail2ban =====
sudo fail2ban-client status sshd                  # 查看SSH jail状态
sudo fail2ban-client set sshd unbanip 1.2.3.4     # 解封IP
sudo fail2ban-client set sshd banip 1.2.3.4       # 封禁IP

# ===== 证书认证 =====
ssh-keygen -s ca_key -I cert_id -n user -V +52w user.pub  # 签发证书
ssh-keygen -L -f cert.pub                         # 查看证书信息

# ===== 权限设置 =====
chmod 700 ~/.ssh                                  # .ssh目录权限
chmod 600 ~/.ssh/authorized_keys                  # authorized_keys权限
chmod 600 ~/.ssh/id_ed25519                       # 私钥权限
chmod 644 ~/.ssh/id_ed25519.pub                   # 公钥权限
chmod 755 ~                                       # 家目录权限上限

 

B. 配置参数详解

sshd_config 关键参数速查

参数 默认值 推荐值 说明
Port 22 52222 SSH监听端口
PermitRootLogin yes no 是否允许root登录
PasswordAuthentication yes no 是否允许密码认证
PubkeyAuthentication yes yes 是否允许公钥认证
MaxAuthTries 6 3 单次连接最大认证尝试次数
LoginGraceTime 120 30 认证超时时间(秒)
MaxSessions 10 5 单连接最大会话数
MaxStartups 10100 1060 未认证连接限制
ClientAliveInterval 0 300 心跳探测间隔(秒)
ClientAliveCountMax 3 3 心跳失败断开阈值
UseDNS yes no 是否做DNS反向解析
GSSAPIAuthentication yes no 是否启用GSSAPI认证
X11Forwarding yes no 是否允许X11转发
AllowAgentForwarding yes no 是否允许Agent转发
LogLevel INFO VERBOSE 日志级别
Banner none /etc/ssh/banner.txt 登录前显示的警告信息
StrictModes yes yes 是否检查文件权限

客户端 ~/.ssh/config 常用参数

参数 说明 示例
HostName 实际主机地址 192.168.1.100
Port SSH端口 52222
User 登录用户名 opsadmin
IdentityFile 私钥文件路径 ~/.ssh/id_ed25519
IdentitiesOnly 只用指定密钥 yes
ProxyJump 跳板机 jump
ServerAliveInterval 心跳间隔(秒) 60
ControlMaster 连接复用 auto
ControlPath 复用socket路径 ~/.ssh/sockets/%r@%h-%p
ControlPersist 复用保持时间(秒) 600
Compression 压缩传输 yes
ForwardAgent Agent转发 no

C. 术语表

术语 英文 解释
非对称加密 Asymmetric Encryption 使用公钥加密、私钥解密的加密方式,SSH密钥认证的基础
公钥 Public Key 可以公开分发的密钥,放在服务端的authorized_keys中
私钥 Private Key 必须严格保密的密钥,存放在客户端,权限必须是600
密钥指纹 Key Fingerprint 密钥的哈希摘要,用于快速识别和验证密钥身份
CA Certificate Authority 证书颁发机构,SSH证书认证中负责签发和吊销用户证书
跳板机 Jump Host / Bastion Host 作为SSH中转的服务器,内网服务器只允许从跳板机访问
端口转发 Port Forwarding 通过SSH隧道将本地端口映射到远程端口,或反向映射
Agent转发 Agent Forwarding 将本地ssh-agent转发到远程服务器,实现多跳免密
连接复用 Connection Multiplexing 多个SSH会话共用一个TCP连接,减少握手开销
fail2ban fail2ban 入侵防御工具,监控日志并自动封禁恶意IP
GSSAPI Generic Security Services API 通用安全服务接口,用于Kerberos认证集成
SELinux Security-Enhanced Linux 安全增强Linux,强制访问控制机制,影响SSH端口和文件访问

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分