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端口和文件访问 |
全部0条评论
快来发表一下你的评论吧 !