Linux 与系统

服务器运维笔记:SSH 安全加固与跳板机

SSH 是运维工程师每天都要打交道的工具,多数人停留在 ssh user@host 这一层。本文从密钥管理、公钥部署、服务端加固、fail2ban 防御、跳板机架构、隧道转发到批量运维,系统梳理 SSH 进阶技能。

SSH 是运维工程师每天都要打交道的工具,但大多数人只停留在 ssh user@host 这一层。本文从密钥管理、公钥部署、服务端加固、fail2ban 防御、跳板机架构、隧道转发到批量运维,系统梳理 SSH 进阶技能。

暴露在公网的服务器,SSH 的默认配置会在 auth 日志里留下持续不断的爆破尝试:22 端口加密码登录相当于把入口敞开。安全加固要叠加多层关卡:修改端口避开自动化扫描、防火墙限制来源 IP、只允许密钥登录、fail2ban 封禁失败尝试。每一层都在抬高攻击成本。


SSH 密钥管理

Ed25519 vs RSA:该选哪个?

对比项 Ed25519 RSA (4096-bit)
密钥长度 256-bit(固定) 2048 / 4096-bit
安全强度 ≈ RSA-3072 取决于位数
性能 签名/验证更快 较慢
兼容性 OpenSSH 6.5+(2014 年起) 几乎所有版本
推荐场景 默认首选 兼容老旧系统

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

密钥生成

密钥对在你自己的电脑上生成,落到 VPS 上的只有公钥:

plaintext8 行
# 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
参数 含义
-t ed25519 指定算法,ed25519 比 RSA 更短更快
-C "..." 注释,便于区分多个密钥
-f path 私钥文件名

生成结果有两个文件:私钥 ~/.ssh/my-vps-key 绝不泄露、绝不上传;公钥 ~/.ssh/my-vps-key.pub 可以分发到所有需要登录的服务器。查看公钥内容用 cat ~/.ssh/my-vps-key.pub,把整行复制下来使用。

passphrase 取舍:交互使用的生产密钥建议始终设置 passphrase;无人值守的自动化或中转场景可单独生成不带 passphrase 的专用密钥,配合 ssh-agent 或凭据保险库管理,避免明文写入配置文件。

最佳实践:

  • 每个环境(dev/staging/prod)使用不同密钥
  • 密钥文件命名体现用途:id_ed25519_prod、id_ed25519_github
  • 私钥按机器隔离:每台开发机生成自己的密钥,服务器 authorized_keys 里按行添加,任何一台机器丢失时只删除对应那一行

ssh-agent 管理密钥

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

plaintext12 行
# 启动 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 集成,密钥重启后自动加载:

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

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

plaintext3 行
Host *
  AddKeysToAgent yes
  UseKeychain yes

密钥轮换

密钥需要管理有效期、撤销和轮换:

plaintext15 行
# 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/authorized_keys,一条公钥占一行,多台设备共用同一文件没有问题。

ssh-copy-id 一键部署

最简单的方式,执行一次输入密码即可:

plaintext1 行
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@<server>

公钥会自动追加到服务器的 ~/.ssh/authorized_keys。

手动追加公钥

商家最小系统未装 ssh-copy-id、或 Windows 上没有对应工具时,先密码登录再手动追加:

plaintext9 行
ssh user@<server>

mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...........xxx user@host" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

# 验证权限
ls -la ~/.ssh/

权限要求

权限过宽时 sshd 会拒绝读取密钥文件,最常见的现象是配置了密钥仍被要求输密码。目录与文件权限必须满足:

plaintext2 行
drwx------ 2 user user .          (.ssh 700)
-rw------- 1 user user authorized_keys   (600)

服务端 ~/.ssh 应为 700、authorized_keys 应为 600;home 目录不能是 777,authorized_keys 的所有者必须是登录用户。

验证密钥登录

新开一个终端验证,保留已经确认可用的原会话作为恢复通道:

plaintext1 行
ssh -i ~/.ssh/id_ed25519 user@<server>

不输密码直接进入即表示成功。


SSH 配置详解

~/.ssh/config 基础

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

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

Host 别名与完整配置示例

plaintext45 行
# === 全局默认配置 ===
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 就能通过跳板机连接数据库服务器,无需手动跳转。

多密钥管理技巧

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

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

KeepAlive 防断连

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

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

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

plaintext2 行
ClientAliveInterval 60
ClientAliveCountMax 3

SSH 安全加固

修改默认端口

把 22 换成高位端口,能过滤掉大部分按默认端口扫描的自动化脚本。先备份原配置:

plaintext1 行
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

在 /etc/ssh/sshd_config 设置新端口(示例为 2222,实际建议在 1024-65535 之间自选):

plaintext1 行
Port 2222

先检查语法再重启。sudo sshd -t 无输出表示配置正确;出现 Bad configuration option 之类错误时不要重启,否则 sshd 起不来,外部访问会被锁在门外:

plaintext1 行
sudo sshd -t

端口放行与重启:

plaintext6 行
sudo ufw allow 2222/tcp
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload

sudo systemctl restart sshd
sudo systemctl status sshd

确认状态是 active (running),再检查监听端口确实变成新端口:

plaintext2 行
sudo ss -tlnp | grep ssh
# LISTEN 0 128 0.0.0.0:2222 0.0.0.0:* users:(("sshd",pid=xxx,fd=3))

验证后删除 22 端口的放行规则。旧终端保留,新开终端用新端口登录,成功进入后再清理:

plaintext2 行
ssh -p 2222 user@<server>
sudo ufw delete allow 22

云厂商用平台控制台代理 SSH 时,入口往往固定监听 22,改端口后要在厂商侧同步调整安全组与入口配置。

限制登录用户

plaintext13 行
# /etc/ssh/sshd_config

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

# 或者按组限制
AllowGroups ssh-users

# 禁止 root 直接登录
PermitRootLogin no

# 禁止空密码
PermitEmptyPasswords no

用密钥登录且需要保留 root 时,可使用 PermitRootLogin prohibit-password,root 只能通过密钥进入,密码登录被禁用。

限制来源 IP

plaintext5 行
# 只允许办公网段和跳板机 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

限制认证尝试

plaintext13 行
# /etc/ssh/sshd_config

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

# 登录宽限时间 30 秒
LoginGraceTime 30

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

# 关闭挑战响应认证
ChallengeResponseAuthentication no

服务端配置对照表

修改前后逐个核对语义:

配置项 改前 改后 作用
Port 22 2222 避开自动化扫描
PubkeyAuthentication yes/no yes 启用密钥
PasswordAuthentication yes no 关闭密码登录
PermitEmptyPasswords no/yes no 禁止空密码
PermitRootLogin yes prohibit-password root 只允许密钥
MaxAuthTries 6 3 限制单次连接的尝试次数

完整的加固配置模板

plaintext33 行
# /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

fail2ban 自动封禁

密钥登录和改端口都挡不住攻击者扫到新端口后继续尝试。fail2ban 监控认证日志,多次失败后按规则封禁来源 IP 一段时间。

安装并写入 /etc/fail2ban/jail.local:

plaintext1 行
sudo apt install fail2ban
plaintext12 行
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
backend = auto

# 家庭/办公 IP 加白名单,避免误封自己
ignoreip = 127.0.0.1/8 ::1 <你的家庭 IP>

logpath 随发行版不同:基于 auth 日志的系统写 /var/log/auth.log,基于 journald 的发行版按 journal 配置相应过滤。

启动并验证:

plaintext2 行
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban

查看封禁状态,手动封禁或解封:

plaintext4 行
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd banip 1.2.3.4
sudo fail2ban-client set sshd unbanip 1.2.3.4

fail2ban 对 SSH 爆破有效,但防不了零日漏洞,保持系统更新才是根本。


跳板机 / Bastion Host

架构设计

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

跳板机职责:

  • 所有对内网的 SSH 访问必须经过跳板机
  • 跳板机上记录所有会话日志(审计)
  • 内网服务器只允许来自跳板机的 SSH 连接
  • 跳板机自身需要严格加固

ProxyJump 配置(OpenSSH 7.3+,推荐)

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

plaintext23 行
# 命令行方式
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 自动通过跳板机连接。

ProxyCommand 配置(兼容旧版本)

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

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

跳板机安全加固

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

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

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

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

会话记录脚本示例:

plaintext6 行
#!/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 最被低估的功能之一,可以安全地穿透防火墙、暴露内网服务。

本地转发(Local Forwarding)

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

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

plaintext8 行
# 将本地 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:本地转发

远程转发(Remote Forwarding)

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

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

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

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

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

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

动态转发 — SOCKS 代理

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

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

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

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

实战场景:

  • 访问公司内网的 Web 应用
  • 安全地使用公共 WiFi
  • 调试远程环境的 Web 服务

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

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

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

SSH 端口转发进阶

多级跳板隧道

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

plaintext23 行
# 方法一: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

隧道保活

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

plaintext5 行
# 方法一: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

autossh — 自动重连的隧道

autossh 在 SSH 连接断开时自动重新建立,是长期运行隧道的优选工具。先安装:

plaintext2 行
sudo apt install autossh    # Debian/Ubuntu
sudo yum install autossh    # CentOS/RHEL

基本用法:

plaintext7 行
autossh -M 20000 -fNL 13306:10.0.2.100:3306 user@bastion

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

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

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

systemd 管理 autossh 隧道:

plaintext15 行
# /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
plaintext3 行
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-mysql
sudo systemctl status ssh-tunnel-mysql

SSH 批量管理

PSSH(Parallel SSH)

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

plaintext22 行
# 安装
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"

mussh(Multihost SSH)

plaintext8 行
# 安装
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"

结合 Ansible 做批量管理

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

plaintext12 行
# 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'
plaintext9 行
# 批量执行命令
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

GUI 客户端接入

Termius、Tabby 等客户端的“添加密钥”流程原理一致:

  1. 生成密钥:在客户端里 Keys → Generate Key
  2. 导入私钥:把本地私钥整个文件内容粘贴进去(只贴公钥不够)
  3. 绑定到主机:在主机配置里选这个 key

私钥留在客户端本地,公钥放在服务器的 ~/.ssh/authorized_keys。不同客户端 UI 略有差异,原则相同。


SSH 日志审计

日志位置

发行版 日志路径
Ubuntu/Debian (systemd) journalctl -u ssh 或 /var/log/auth.log
CentOS/RHEL 7+ /var/log/secure
CentOS/RHEL 8+ (journald) journalctl -u sshd
plaintext12 行
# 查看最近的 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

增强审计配置

plaintext2 行
# /etc/ssh/sshd_config
LogLevel VERBOSE

VERBOSE 级别会记录:

  • 每次认证尝试使用的密钥指纹
  • 密钥认证成功/失败的原因
  • 会话的详细信息

登录告警脚本

plaintext28 行
#!/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 模块自动触发:

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

关键文件与权限一览

文件 作用 权限
本地 ~/.ssh/id_ed25519 私钥 600
本地 ~/.ssh/id_ed25519.pub 公钥 644
本地 ~/.ssh/config 客户端配置 600
服务器 /etc/ssh/sshd_config 服务端配置 644
服务器 ~/.ssh/authorized_keys 已授权公钥列表 600
服务器 /var/log/auth.log SSH 登录日志 -
服务器 /etc/fail2ban/jail.local fail2ban 配置 -

常见问题排查

连接超时

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

常见原因及解决:

  1. 防火墙阻断:telnet host 22 或 nc -zv host 22 测试端口
  2. DNS 解析慢:在 /etc/ssh/sshd_config 设置 UseDNS no
  3. GSSAPI 认证慢:在客户端配置 GSSAPIAuthentication no
  4. 网络 MTU 问题:ping -M do -s 1400 host 测试

权限问题

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

plaintext14 行
# 服务端
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

密钥不匹配

plaintext12 行
# 查看服务器端接受的日志
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

Agent Forwarding 问题

plaintext13 行
# 检查 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 或 authorized_keys 权限不对 chmod 700 ~/.ssh; chmod 600 authorized_keys
改端口后连不上 防火墙没放新端口 控制台或 VNC 进入后 ufw allow 新端口
sshd restart 后服务起不来 配置语法错误 重启前用 sshd -t 检查
改配置没生效 没重启 sshd systemctl restart sshd
改端口没生效(仍 22) 云厂商平台代理 SSH 用 ss -lntp 确认监听者
fail2ban 把自己封了 没加 ignoreip 控制台进入后解封自己的 IP
部分轻量服务器没法改端口 平台层硬编码 22 去厂商控制台调整

SSH 最佳实践清单

密钥管理

  • 使用 Ed25519 密钥(RSA 至少 4096-bit)
  • 每个密钥设置 passphrase
  • 不同环境使用不同密钥
  • 定期轮换密钥(建议 90 天)
  • 使用 ssh-agent 管理密钥,设置自动过期时间
  • 离职人员密钥立即清除

安全加固

  • 修改默认端口
  • 禁止 root 直接登录
  • 禁止密码认证,只允许密钥
  • 限制可登录的用户/组
  • 限制来源 IP(防火墙 + sshd 配置双重限制)
  • 设置 MaxAuthTries(≤5)和 LoginGraceTime(≤60)
  • 使用强加密算法(Ciphers / MACs / KexAlgorithms)
  • 安装 fail2ban 自动封禁暴力破解
  • 高危环境可叠加 2FA(google-authenticator-libpam 做一次性验证码)
  • 保持 OpenSSH 版本更新

运维效率

  • 善用 ~/.ssh/config 管理连接
  • 配置 ServerAliveInterval 防止断连
  • 跳板机使用 ProxyJump 管理跳转路径
  • 长期隧道使用 autossh + systemd
  • 批量管理使用 Ansible
  • 启用日志审计,配置登录告警

日常习惯

  • 定期检查 last 和 lastb 查看登录记录
  • 定期审查 authorized_keys 清理过期密钥
  • 不在代码仓库中存储私钥
  • 不在多台机器之间共享同一私钥
  • 使用 ssh-keygen -l -f 验证密钥指纹

附录:速查命令表

plaintext28 行
# 密钥生成
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% 的生产运维场景。关键不在于记住所有命令,而在于理解背后的原理、知道遇到问题时去哪里查、怎么查。

跳转、主机身份与会话验证

ProxyJump 让本机客户端经跳板建立目标连接。目标主机的身份仍由 SSH 主机密钥验证;遇到 host key 变化先通过可信渠道核对变更原因,避免把删除 known_hosts 当作固定修复。

修改配置先用 sshd -t 检查,再保留原会话创建新连接,分别验证普通登录、sudo 和跳板访问。停用旧密钥时同时核查所有账户与自动化任务,撤销流程要能定位密钥所属人和用途。

← 返回文章