SSH 是运维工程师每天都要打交道的工具,但大多数人只停留在 ssh user@host 这一层。本文从密钥管理、安全加固、跳板机架构、隧道转发到批量运维,系统梳理 SSH 进阶技能,帮你从"会用"升级到"用好"。


一、SSH 密钥管理

1.1 Ed25519 vs RSA:该选哪个?

对比项

Ed25519

RSA (4096-bit)

密钥长度

256-bit(固定)

2048 / 4096-bit

安全强度

≈ RSA-3072

取决于位数

性能

签名/验证更快

较慢

兼容性

OpenSSH 6.5+(2014 年起)

几乎所有版本

推荐场景

默认首选

兼容老旧系统

结论:新环境一律用 Ed25519,除非目标机器的 OpenSSH 版本低于 6.5。

1.2 密钥生成

# Ed25519(推荐)
ssh-keygen -t ed25519 -C "your_name@company.com"

# RSA 4096-bit(兼容性兜底)
ssh-keygen -t rsa -b 4096 -C "your_name@company.com"

# 带 passphrase 的密钥(生产环境必须)
ssh-keygen -t ed25519 -C "ops@prod" -f ~/.ssh/id_ed25519_prod

最佳实践

  • 每个环境(dev/staging/prod)使用不同密钥

  • 密钥文件命名体现用途:id_ed25519_prodid_ed25519_github

  • 始终设置 passphrase

1.3 ssh-agent 管理密钥

ssh-agent 是一个后台进程,缓存解密后的私钥,避免每次连接都输入 passphrase。

# 启动 agent 并添加密钥
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

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

# 删除所有已加载密钥
ssh-add -D

# 设置密钥自动过期(3600 秒后从 agent 中移除)
ssh-add -t 3600 ~/.ssh/id_ed25519_prod

macOS 用户提示:macOS 的 ssh-agent 支持 Keychain 集成,密钥重启后自动加载:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

~/.ssh/config 中配置自动使用 Keychain:

Host *
  AddKeysToAgent yes
  UseKeychain yes

1.4 密钥轮换

密钥不是一劳永逸的,需要定期轮换:

# 1. 生成新密钥
ssh-keygen -t ed25519 -C "ops@2026-rotate" -f ~/.ssh/id_ed25519_new

# 2. 部署新公钥到目标机器
ssh-copy-id -i ~/.ssh/id_ed25519_new.pub user@target

# 3. 验证新密钥可用
ssh -i ~/.ssh/id_ed25519_new user@target "echo ok"

# 4. 从目标机器移除旧公钥
ssh user@target "sed -i '/old_key_comment/d' ~/.ssh/authorized_keys"

# 5. 本地替换旧密钥
mv ~/.ssh/id_ed25519_new ~/.ssh/id_ed25519
mv ~/.ssh/id_ed25519_new.pub ~/.ssh/id_ed25519.pub

轮换策略建议

  • 生产环境每 90 天轮换一次

  • 离职人员的密钥立即清除

  • 使用配置管理工具(Ansible)批量部署新公钥


二、SSH 配置详解

2.1 ~/.ssh/config 基础

SSH 客户端配置文件是提升效率的利器,一次配置,终身受益。

# 文件权限必须正确
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config

2.2 Host 别名与完整配置示例

# === 全局默认配置 ===
Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3
  AddKeysToAgent yes
  IdentitiesOnly yes
  Compression yes
  HashKnownHosts yes

# === 生产环境 Web 服务器 ===
Host prod-web
  HostName 10.0.1.50
  User deploy
  Port 2222
  IdentityFile ~/.ssh/id_ed25519_prod
  StrictHostKeyChecking ask

# === GitHub ===
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_github
  IdentitiesOnly yes

# === GitLab(公司内部)===
Host gitlab-internal
  HostName gitlab.company.com
  User git
  IdentityFile ~/.ssh/id_ed25519_gitlab
  Port 2222

# === 生产数据库(通过跳板机)===
Host prod-db
  HostName 10.0.2.100
  User dbadmin
  ProxyJump bastion
  IdentityFile ~/.ssh/id_ed25519_prod

# === 跳板机 ===
Host bastion
  HostName bastion.company.com
  User ops
  Port 2222
  IdentityFile ~/.ssh/id_ed25519_bastion
  ForwardAgent yes

配置完成后,直接 ssh prod-db 就能通过跳板机连接数据库服务器,无需手动跳转。

2.3 多密钥管理技巧

当一台机器有多个密钥时,IdentitiesOnly yes 很关键——它告诉 SSH 只使用配置文件中指定的密钥,不会尝试 agent 中的其他密钥,避免因尝试过多密钥被服务器拒绝。

# 测试哪个密钥能连接某台服务器
ssh -vT git@github.com 2>&1 | grep "Offering\|Accepted"

2.4 KeepAlive 防断连

NAT 网关、防火墙会杀掉空闲连接,KeepAlive 是必备配置:

Host *
  # 每 60 秒发送一次心跳
  ServerAliveInterval 60
  # 连续 3 次无响应则断开
  ServerAliveCountMax 3

服务端也需要配合(/etc/ssh/sshd_config):

ClientAliveInterval 60
ClientAliveCountMax 3

三、SSH 安全加固

3.1 修改默认端口

最简单的"防君子不防小人"的措施,但能过滤掉 99% 的自动化扫描:

# /etc/ssh/sshd_config
Port 2222
# 重启服务
sudo systemctl restart sshd

# 别忘了防火墙放行
sudo ufw allow 2222/tcp
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload

3.2 限制登录用户

# /etc/ssh/sshd_config

# 只允许指定用户登录
AllowUsers deploy ops admin

# 或者按组限制
AllowGroups ssh-users

# 禁止 root 直接登录
PermitRootLogin no

# 禁止空密码
PermitEmptyPasswords no

3.3 限制来源 IP

# 只允许办公网段和跳板机 IP
AllowUsers deploy@10.0.0.0/24 deploy@203.0.113.50

# 或者用防火墙层面限制
sudo ufw allow from 10.0.0.0/24 to any port 2222

3.4 限制认证尝试

# /etc/ssh/sshd_config

# 最多尝试 3 次密码/密钥认证
MaxAuthTries 3

# 登录宽限时间 30 秒
LoginGraceTime 30

# 关闭密码认证(只允许密钥)
PasswordAuthentication no

# 关闭挑战响应认证
ChallengeResponseAuthentication no

3.5 完整的加固配置模板

# /etc/ssh/sshd_config — 安全加固版

Port 2222
Protocol 2
AddressFamily inet

PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no

MaxAuthTries 3
LoginGraceTime 30
MaxSessions 5
MaxStartups 10:30:60

AllowGroups ssh-users
AllowUsers deploy ops

X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no

UsePAM yes
PrintMotd no

# 日志级别
LogLevel VERBOSE

# 使用强加密算法
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org

四、跳板机 / Bastion Host

4.1 架构设计

┌──────────┐     SSH      ┌────────────┐    SSH    ┌──────────────┐
│  运维PC   │ ──────────→ │  跳板机      │ ───────→ │  内网服务器    │
│ (公网)    │             │ (Bastion)   │          │  (私有网络)   │
└──────────┘             └────────────┘          └──────────────┘

跳板机职责

  • 所有对内网的 SSH 访问必须经过跳板机

  • 跳板机上记录所有会话日志(审计)

  • 内网服务器只允许来自跳板机的 SSH 连接

  • 跳板机自身需要严格加固

4.2 ProxyJump 配置(OpenSSH 7.3+,推荐)

ProxyJump 是现代方式,简洁且安全:

# 命令行方式
ssh -J bastion:2222 user@internal-host

# 多级跳板
ssh -J bastion1:2222,bastion2:2222 user@final-host

# ~/.ssh/config 配置方式
Host bastion
  HostName bastion.company.com
  User ops
  Port 2222
  IdentityFile ~/.ssh/id_ed25519_bastion

Host internal-*
  ProxyJump bastion
  User deploy
  IdentityFile ~/.ssh/id_ed25519_internal

Host internal-web
  HostName 10.0.1.50

Host internal-db
  HostName 10.0.2.100

配置后,ssh internal-web 自动通过跳板机连接。

4.3 ProxyCommand 配置(兼容旧版本)

# ~/.ssh/config
Host internal-host
  HostName 10.0.1.50
  User deploy
  ProxyCommand ssh -W %h:%p bastion

-W %h:%p 表示将标准输入/输出转发到目标主机:端口,效果等同于 ProxyJump。

4.4 跳板机安全加固

# 跳板机的 sshd_config 特殊配置
Port 2222
PermitRootLogin no
PasswordAuthentication no

# 允许 TCP 转发(ProxyJump 需要)
AllowTcpForwarding yes

# 禁止在跳板机上开 shell(可选,极端安全场景)
# ForceCommand /bin/false

# 记录所有通过跳板机的会话
ForceCommand /usr/bin/log-session.sh

会话记录脚本示例:

#!/bin/bash
# /usr/bin/log-session.sh
LOG_DIR="/var/log/ssh-sessions"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(date +%Y%m%d-%H%M%S)-${USER}-$(echo $SSH_CLIENT | awk '{print $1}').log"
script -qf "$LOG_FILE"

五、SSH 隧道

SSH 隧道是 SSH 最被低估的功能之一,可以安全地穿透防火墙、暴露内网服务。

5.1 本地转发(Local Forwarding)

将本地端口映射到远程服务器的某个端口。

场景:访问内网的 MySQL 数据库(10.0.2.100:3306),但你的 PC 无法直接连接。

# 将本地 13306 端口转发到内网 MySQL
ssh -L 13306:10.0.2.100:3306 user@bastion

# 然后用本地客户端连接
mysql -h 127.0.0.1 -P 13306 -u root -p

# 后台运行,不打开 shell
ssh -fNL 13306:10.0.2.100:3306 user@bastion

参数说明:

  • -f:后台运行

  • -N:不执行远程命令

  • -L [bind_addr:]port:host:hostport:本地转发

5.2 远程转发(Remote Forwarding)

将远程服务器的端口映射到本地,让外部能访问你本地的服务。

场景:你在内网开发了一个 Web 服务(localhost:3000),想让同事通过公网服务器访问。

# 将公网服务器的 8080 端口转发到你本地的 3000 端口
ssh -R 8080:localhost:3000 user@public-server

# 同事访问 http://public-server:8080 即可看到你的服务

注意:远程转发默认只绑定 loopback 地址,要让外部访问需要在服务端配置:

# /etc/ssh/sshd_config
GatewayPorts yes   # 或 clientspecified

5.3 动态转发 — SOCKS 代理

将 SSH 变成一个 SOCKS5 代理,所有流量通过远程服务器中转。

# 启动 SOCKS5 代理,监听本地 1080 端口
ssh -D 1080 user@remote-server

# 后台运行
ssh -fND 1080 user@remote-server

然后在浏览器或系统中配置 SOCKS5 代理:127.0.0.1:1080

实战场景

  • 访问公司内网的 Web 应用

  • 安全地使用公共 WiFi

  • 调试远程环境的 Web 服务

5.4 实战组合:通过跳板机访问内网 Web 控制台

# 通过跳板机将本地 8443 映射到内网 K8s Dashboard
ssh -J bastion -L 8443:10.0.3.50:6443 user@internal-host

# 浏览器打开 https://localhost:8443

六、SSH 端口转发进阶

6.1 多级跳板隧道

场景:需要经过两层跳板机才能访问目标服务器。

# 方法一:ProxyJump 链式跳转(推荐)
ssh -J user1@bastion1:2222,user2@bastion2:2222 user3@target

# 方法二:多级本地转发
ssh -L 2222:bastion2:2222 user1@bastion1 &
ssh -L 3306:target-db:3306 -p 2222 user2@localhost &

# 方法三:在 ~/.ssh/config 中配置
Host bastion1
  HostName bastion1.company.com
  User ops1
  Port 2222

Host bastion2
  HostName bastion2.company.com
  User ops2
  Port 2222
  ProxyJump bastion1

Host target
  HostName 10.0.3.100
  User deploy
  ProxyJump bastion2

6.2 隧道保活

长时间运行的隧道容易因网络波动断开,需要保活机制:

# 方法一:ServerAlive 配置(客户端侧)
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -L 3306:db:3306 user@bastion

# 方法二:TCPKeepAlive(更底层)
ssh -o TCPKeepAlive=yes -o ServerAliveInterval=30 -L 3306:db:3306 user@bastion

6.3 autossh — 自动重连的隧道

autossh 在 SSH 连接断开时自动重新建立,是长期运行隧道的必备工具。

# 安装
sudo apt install autossh    # Debian/Ubuntu
sudo yum install autossh    # CentOS/RHEL

# 基本用法
autossh -M 20000 -fNL 13306:10.0.2.100:3306 user@bastion

# 参数说明
# -M 20000: 使用 20000 端口做连接监控(偶数端口发,奇数端口收)
# -f: 后台运行
# -N: 不执行远程命令
# -L: 本地转发

更优雅的用法(使用 ServerAlive 替代 -M 端口监控):

autossh -M 0 -fNL 13306:10.0.2.100:3306 user@bastion \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3"

systemd 管理 autossh 隧道

# /etc/systemd/system/ssh-tunnel-mysql.service
[Unit]
Description=SSH Tunnel to MySQL
After=network-online.target
Wants=network-online.target

[Service]
User=ops
Environment="AUTOSSH_GATETIME=0"
ExecStart=/usr/bin/autossh -M 0 -NL 13306:10.0.2.100:3306 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 user@bastion
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-mysql
sudo systemctl status ssh-tunnel-mysql

七、SSH 批量管理

7.1 PSSH(Parallel SSH)

并行执行 SSH 命令,适合小规模服务器群。

# 安装
sudo apt install pssh    # Debian/Ubuntu
sudo yum install pssh    # CentOS/RHEL

# 创建主机列表
cat > hosts.txt << 'EOF'
web1 ansible_host=10.0.1.11 ansible_user=deploy
web2 ansible_host=10.0.1.12 ansible_user=deploy
web3 ansible_host=10.0.1.13 ansible_user=deploy
EOF

# 并行执行命令
pssh -h hosts.txt -i "uptime"

# 并行复制文件
pscp -h hosts.txt /tmp/app.conf /opt/app/conf/

# 并行同步目录
prsync -h hosts.txt -r /opt/app/ /opt/app/

# 带日志输出
pssh -h hosts.txt -o /tmp/pssh-out -e /tmp/pssh-err "df -h"

7.2 mussh(Multihost SSH)

# 安装
sudo apt install mussh

# 对多台主机执行命令
mussh -H "10.0.1.11 10.0.1.12 10.0.1.13" -c "systemctl restart nginx"

# 使用主机文件
mussh -H hosts.txt -c "free -m"

7.3 结合 Ansible 做批量管理

Ansible 底层就是 SSH,适合更复杂的批量运维场景:

# inventory/hosts.yml
all:
  children:
    webservers:
      hosts:
        web1: { ansible_host: 10.0.1.11 }
        web2: { ansible_host: 10.0.1.12 }
        web3: { ansible_host: 10.0.1.13 }
  vars:
    ansible_user: deploy
    ansible_ssh_private_key_file: ~/.ssh/id_ed25519_prod
    ansible_ssh_common_args: '-o ProxyJump=bastion'
# 批量执行命令
ansible webservers -m shell -a "df -h"

# 批量部署配置
ansible webservers -m template -a "src=nginx.conf.j2 dest=/etc/nginx/nginx.conf"

# 批量更新并重启服务
ansible webservers -m apt -a "name=nginx state=latest" --become
ansible webservers -m service -a "name=nginx state=restarted" --become

八、SSH 日志审计

8.1 日志位置

发行版

日志路径

Ubuntu/Debian (systemd)

journalctl -u ssh/var/log/auth.log

CentOS/RHEL 7+

/var/log/secure

CentOS/RHEL 8+ (journald)

journalctl -u sshd

# 查看最近的 SSH 登录
journalctl -u ssh --since "1 hour ago"

# 查看失败的登录尝试
grep "Failed password" /var/log/auth.log

# 统计每个 IP 的失败尝试次数
grep "Failed password" /var/log/auth.log | \
  awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

# 查看成功的登录
grep "Accepted" /var/log/auth.log

8.2 增强审计配置

# /etc/ssh/sshd_config
LogLevel VERBOSE

VERBOSE 级别会记录:

  • 每次认证尝试使用的密钥指纹

  • 密钥认证成功/失败的原因

  • 会话的详细信息

8.3 登录告警脚本

#!/bin/bash
# /etc/ssh/login-notify.sh
# 在 /etc/ssh/sshd_config 中配置:
# PermitUserEnvironment yes
# 然后在 ~/.ssh/environment 中设置触发

LOG_FILE="/var/log/ssh-login-alerts.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')

# 获取登录信息
USER=$1
IP=$2
TTY=$3

# 写入日志
echo "[$DATE] SSH Login: User=$USER IP=$IP TTY=$TTY" >> "$LOG_FILE"

# 发送告警(示例:通过 webhook 发送到飞书/钉钉/Slack)
curl -s -X POST "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_HOOK_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"msg_type\": \"text\",
    \"content\": {
      \"text\": \"⚠️ SSH 登录告警\n用户: $USER\nIP: $IP\n时间: $DATE\"
    }
  }" &

exit 0

配合 PAM 模块自动触发:

# /etc/pam.d/sshd 末尾添加
session optional pam_exec.so /etc/ssh/login-notify.sh

8.4 使用 fail2ban 自动封禁

# 安装
sudo apt install fail2ban

# 配置
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
EOF

sudo systemctl enable --now fail2ban

# 查看被封禁的 IP
sudo fail2ban-client status sshd

# 手动解封
sudo fail2ban-client set sshd unbanip 192.168.1.100

九、常见问题排查

9.1 连接超时

# 详细模式查看卡在哪一步
ssh -vvv user@host

常见原因及解决:

  1. 防火墙阻断telnet host 22nc -zv host 22 测试端口

  2. DNS 解析慢:在 /etc/ssh/sshd_config 设置 UseDNS no

  3. GSSAPI 认证慢:在客户端配置 GSSAPIAuthentication no

  4. 网络 MTU 问题ping -M do -s 1400 host 测试

9.2 权限问题

SSH 对文件权限非常严格,权限不对直接拒绝:

# 服务端
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 755 ~          # home 目录不能是 777

# 客户端
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/config

# 检查所有者
ls -la ~/.ssh/
# authorized_keys 必须属于当前用户,不能是 root

9.3 密钥不匹配

# 查看服务器端接受的日志
ssh -vT user@host 2>&1 | grep -i "key\|identity\|offer\|accept"

# 验证公钥指纹是否一致
ssh-keygen -lf ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519

# 在服务器端检查
cat ~/.ssh/authorized_keys | ssh-keygen -lf -

# 确保公钥格式正确(单行,以 ssh-ed25519 或 ssh-rsa 开头)
cat ~/.ssh/id_ed25519.pub

9.4 Agent Forwarding 问题

# 检查 agent 是否运行
ssh-add -l

# 检查 agent forwarding 是否启用
ssh -o ForwardAgent=yes user@host "echo \$SSH_AUTH_SOCK"

# 如果 AUTH_SOCK 为空,检查:
# 1. 客户端 ~/.ssh/config 中 ForwardAgent yes
# 2. 服务端 /etc/ssh/sshd_config 中 AllowAgentForwarding yes
# 3. 服务端 ~/.ssh/environment 中 SSH_AUTH_SOCK 设置

# 调试:在中间机器上测试
ssh -A bastion "ssh-add -l && ssh internal-host 'whoami'"

安全警告:Agent Forwarding 有风险——root 用户可以劫持你的 agent 来连接其他机器。在不信任的服务器上不要开启。


十、SSH 最佳实践清单

密钥管理

  • 使用 Ed25519 密钥(RSA 至少 4096-bit)

  • 每个密钥设置 passphrase

  • 不同环境使用不同密钥

  • 定期轮换密钥(建议 90 天)

  • 使用 ssh-agent 管理密钥,设置自动过期时间

  • 离职人员密钥立即清除

安全加固

  • 修改默认端口

  • 禁止 root 直接登录

  • 禁止密码认证,只允许密钥

  • 限制可登录的用户/组

  • 限制来源 IP(防火墙 + sshd 配置双重限制)

  • 设置 MaxAuthTries(≤5)和 LoginGraceTime(≤60)

  • 使用强加密算法(Ciphers / MACs / KexAlgorithms)

  • 安装 fail2ban 自动封禁暴力破解

  • 保持 OpenSSH 版本更新

运维效率

  • 善用 ~/.ssh/config 管理连接

  • 配置 ServerAliveInterval 防止断连

  • 跳板机使用 ProxyJump(而非手动跳转)

  • 长期隧道使用 autossh + systemd

  • 批量管理使用 Ansible

  • 启用日志审计,配置登录告警

日常习惯

  • 定期检查 last 和 lastb 查看登录记录

  • 定期审查 authorized_keys 清理过期密钥

  • 不在代码仓库中存储私钥

  • 不在多台机器之间共享同一私钥

  • 使用 ssh-keygen -l -f 验证密钥指纹


附录:速查命令表

# 密钥生成
ssh-keygen -t ed25519 -C "comment"

# 部署公钥
ssh-copy-id -i ~/.ssh/key.pub user@host

# 跳板机连接
ssh -J bastion user@internal

# 本地转发
ssh -L local_port:target:remote_port user@via

# 远程转发
ssh -R remote_port:localhost:local_port user@public

# SOCKS 代理
ssh -D 1080 user@host

# 自动重连隧道
autossh -M 0 -fNL 3306:db:3306 user@bastion

# 并行执行
pssh -h hosts.txt -i "command"

# 查看登录日志
journalctl -u ssh --since "1 hour ago"
last -a
lastb -a

本文为《服务器运维笔记》系列篇。SSH 的进阶用法远不止这些,但掌握以上内容足以应对 95% 的生产运维场景。关键不在于记住所有命令,而在于理解背后的原理,遇到问题时知道去哪里查、怎么查。