前言
ssh user@server大概是开发者敲得最多的远程命令之一。但很多人对 SSH 的理解停留在"能登录就行"的阶段------不知道为什么第一次连接会问Are you sure you want to continue connecting,不知道密钥认证到底怎么工作,更不知道 SSH 除了登录还能做端口转发和跳板机。SSH(Secure Shell)不只是一个"远程登录工具",它是一套加密通信协议。理解了它的底层机制,你才能用好密钥管理、端口转发、代理跳转这些进阶能力,也才能给自己的服务器做出真正有效的安全加固。
一、SSH 连接到底发生了什么
当你敲下 ssh user@192.168.1.100 到最终看到远程 shell 提示符,中间经历了三个阶段。
阶段一:版本协商与密钥交换
客户端和服务器先确认彼此支持的 SSH 协议版本。目前 SSH-2 是唯一推荐使用的版本(SSH-1 有已知的中间人攻击漏洞)。然后双方通过 Diffie-Hellman 密钥交换算法协商出一个临时的对称会话密钥。
这一步的精妙之处在于:会话密钥从未在网络上传输过。客户端和服务器各自独立计算出相同的密钥,即使有人在中间监听全部通信,也无法推算出这个密钥。这就是 Diffie-Hellman 算法的核心价值。

图1:SSH 协议的三层架构。传输层负责密钥交换和加密,认证层负责验证用户身份,连接层负责管理多个逻辑通道(如 shell、端口转发)。
阶段二:服务器身份验证
密钥交换完成后,客户端需要验证"对面真的是我要连的那台服务器吗"。服务器会发送自己的主机密钥(Host Key) ,客户端将它与本地 ~/.ssh/known_hosts 文件中的记录比对。
第一次连接时,known_hosts 里没有这条记录,所以你看到了那个经典提示:
The authenticity of host '192.168.1.100' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no)?
输入 yes 后,服务器的公钥指纹会被写入 known_hosts。下次连接时,如果服务器密钥和记录不一致(比如服务器重装了、或者有人在中间劫持),SSH 会给出一个非常醒目的警告,阻止你继续连接。
所以那个
yes/no不是走形式。它是 SSH 防中间人攻击的第一道防线。如果你连的是自己的服务器,第一次yes是正常的;如果你连过很多次突然弹这个提示,要警惕。
阶段三:用户认证
服务器身份确认后,轮到客户端证明自己是谁。常见的认证方式有两种:
- 密码认证:客户端把密码加密后发给服务器验证。简单但不安全------密码可能被暴力破解。
- 公钥认证:客户端用私钥对一个挑战值签名,服务器用存储的公钥验证签名。密码从不离开本机,安全性高得多。
整个连接建立后,所有后续通信都使用阶段一协商出的对称密钥加密(通常用 AES-256 或 ChaCha20)。之所以用对称加密而不是非对称加密传输数据,是因为对称加密速度快几个数量级,适合大量数据传输。非对称加密只用在开头的密钥交换和身份认证环节。
二、密钥认证:从零配置免密登录
公钥认证是 SSH 最推荐的认证方式。它不仅免去了每次输密码的麻烦,更重要的是安全性远超密码认证。
2.1 生成密钥对
bash
# 推荐:使用 Ed25519 算法(安全且性能好)
ssh-keygen -t ed25519 -C "your_email@example.com"
# 如果需要兼容老系统,用 RSA 4096 位
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
执行后会提示你选择保存路径和密码短语(passphrase):
- 保存路径 :默认
~/.ssh/id_ed25519(私钥)和~/.ssh/id_ed25519.pub(公钥),一般直接回车即可。 - Passphrase :给私钥加一层密码保护。即使私钥文件被偷走,攻击者还需要破解这个密码才能使用。强烈建议设置 ,配合
ssh-agent可以避免每次都输入。

图2:SSH 公钥认证流程。客户端用私钥签名挑战值,服务器用 authorized_keys 中存储的公钥验证签名。
2.2 把公钥复制到服务器
bash
# 最简单的方式:ssh-copy-id 一步到位
ssh-copy-id user@192.168.1.100
# 如果 ssh-copy-id 不可用,手动操作
cat ~/.ssh/id_ed25519.pub | ssh user@192.168.1.100 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
ssh-copy-id 会自动在服务器上创建 ~/.ssh 目录、设置正确的权限、把公钥追加到 authorized_keys 文件。手动操作时最容易出错的就是文件权限------SSH 服务器对权限非常敏感。
2.3 用 ssh-agent 管理密钥密码
每次连接都输入 passphrase 很烦。ssh-agent 帮你在内存中缓存已解锁的私钥:
bash
# 启动 ssh-agent(如果系统没有自动启动的话)
eval "$(ssh-agent -s)"
# 把私钥加入 agent(只需输入一次 passphrase)
ssh-add ~/.ssh/id_ed25519
# 查看 agent 中缓存了哪些密钥
ssh-add -l
加入后,当前终端会话中所有 SSH 连接都不需要再输 passphrase。关闭终端后 agent 也随之失效。
macOS 用户可以在 ~/.ssh/config 中添加以下配置,让 Keychain 自动管理:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
三、ssh_config:告别冗长的命令行
每次连接都写 ssh -i ~/.ssh/work_key -p 2222 -o ServerAliveInterval=60 user@server.example.com 太折磨了。~/.ssh/config 文件可以把这些参数固化成简短的别名。
3.1 基本配置示例
# 工作服务器
Host work
HostName 10.0.1.50
User deploy
Port 2222
IdentityFile ~/.ssh/work_ed25519
ServerAliveInterval 60
ServerAliveCountMax 3
# 个人 VPS
Host vps
HostName 47.96.xx.xx
User root
IdentityFile ~/.ssh/vps_ed25519
配置好后,只需 ssh work 或 ssh vps 就能连接。Host 后面的名字就是别名,HostName 才是真正的地址。
3.2 常用配置项
| 配置项 | 含义 | 典型值 |
|---|---|---|
HostName |
服务器 IP 或域名 | 10.0.1.50 |
User |
登录用户名 | deploy |
Port |
SSH 端口(默认 22) | 2222 |
IdentityFile |
私钥文件路径 | ~/.ssh/id_ed25519 |
ServerAliveInterval |
心跳间隔(秒),防断连 | 60 |
ServerAliveCountMax |
心跳无响应最大次数 | 3 |
ForwardAgent |
转发 ssh-agent 到远程 | yes |
ProxyJump |
跳板机(下文详讲) | bastion |
LocalForward |
本地端口转发(下文详讲) | 5432 db:5432 |
LogLevel |
日志级别 | QUIET / VERBOSE |
3.3 通配符与继承
Host 支持通配符,可以为一组服务器设置通用配置:
# 所有 10.0.x.x 的内网机器
Host 10.0.*
User admin
IdentityFile ~/.ssh/internal_key
StrictHostKeyChecking no
# 所有机器都启用的通用设置
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519
匹配规则 :SSH 从上到下扫描配置文件,第一个匹配的 Host 块的参数优先生效。所以具体的 Host 配置写在前面,通配符兜底配置写在后面。
四、端口转发:SSH 隧道三种模式
端口转发是 SSH 最强大的进阶功能之一。它的本质是在 SSH 加密通道中"夹带"其他协议的流量,相当于建了一条加密隧道。有三种模式,解决三种不同的网络问题。
4.1 本地转发(-L):从本地访问远程内网
场景:你的开发机在公司外网,数据库在公司的内网,只有跳板机能访问。你想在本地用 DBeaver 直连内网数据库。
bash
ssh -L 5432:db.internal:5432 user@jump-server
这条命令的意思是:把本地的 5432 端口 的流量,通过 jump-server 的 SSH 隧道,转发到 db.internal:5432。执行后,你在本地连 localhost:5432 就等于连内网数据库。

图3:SSH 本地端口转发示意图。本地端口的流量通过 SSH 加密隧道到达远程服务器,再由远程服务器转发到目标机器。
bash
# 如果不想在远程打开 shell,加 -N 参数
ssh -N -L 5432:db.internal:5432 user@jump-server
# 如果想放到后台运行,加 -f
ssh -N -f -L 5432:db.internal:5432 user@jump-server
4.2 远程转发(-R):让外部访问你的本地服务
场景 :你在本地跑了一个 Web 应用(localhost:3000),想让同事临时访问测试,但你的机器在内网没有公网 IP。
bash
# 从你的内网机器连到公网服务器,建立反向隧道
ssh -R 0.0.0.0:9090:localhost:3000 user@public-server
这条命令的意思是:把公网服务器的 9090 端口 的流量,反向转发到你本地的 3000 端口。同事访问 public-server:9090 就等于访问你的本地服务。
远程转发绑定到
0.0.0.0需要服务器端sshd_config中开启GatewayPorts yes,默认只绑定到127.0.0.1(仅服务器自己可访问)。
4.3 动态转发(-D):搭建 SOCKS5 代理
场景:你想让浏览器或工具的所有流量都通过远程服务器中转,相当于一个轻量级 VPN。
bash
ssh -N -f -D 1080 user@remote-server
执行后,本地 1080 端口变成一个 SOCKS5 代理。在浏览器或应用中设置代理地址为 127.0.0.1:1080,所有流量都会通过远程服务器中转。这在需要绕过网络限制或隐藏真实 IP 时很有用。
端口转发速查表
| 类型 | 参数 | 方向 | 典型场景 |
|---|---|---|---|
| 本地转发 | -L local:target:port |
本地 → 远程内网 | 连内网数据库、访问内部 Web |
| 远程转发 | -R remote:local:port |
远程 → 本地 | 让外部访问本地开发服务 |
| 动态转发 | -D local_port |
本地 → 任意(SOCKS5) | 全局代理、流量中转 |
五、跳板机与 ProxyJump
在公司环境中,内网服务器通常不能直接从外网访问,必须先登录一台"跳板机"(Bastion Host),再从跳板机跳转到目标机器。
传统方式:两次 SSH
bash
# 第一步:登录跳板机
ssh user@jump-server
# 第二步:在跳板机上再 SSH 到目标机器
ssh user@target-internal
麻烦在于:每次都要先进跳板机再跳转,而且无法直接用 scp 传文件到目标机器。
现代方式:ProxyJump(-J)
SSH 7.3 之后引入了 -J 参数,一行命令直达目标:
bash
# 通过跳板机连接目标机器
ssh -J user@jump-server user@target-internal
# 多跳:依次经过两个跳板机
ssh -J user@jump1,user@jump2 user@target-internal
在 ~/.ssh/config 中配置更优雅:
# 跳板机
Host bastion
HostName jump.example.com
User ops
IdentityFile ~/.ssh/bastion_key
# 内网所有机器都通过跳板机访问
Host *.internal
User deploy
ProxyJump bastion
IdentityFile ~/.ssh/internal_key
配好后,ssh web01.internal 就会自动经过 bastion 跳转,对用户完全透明。scp、rsync 等工具也自动走跳板机通道。
在 config 中叠加端口转发
如果每次连跳板机时还要开多个端口转发(比如连 K8s 集群的多个服务),可以全部写进 config:
Host k8s-dev
HostName 10.0.0.1
ProxyJump bastion
LocalForward 8080 nginx.cluster:80
LocalForward 5432 postgres.cluster:5432
LocalForward 6379 redis.cluster:6379
DynamicForward 1080
一条 ssh -N k8s-dev 命令就能同时建立所有隧道。
六、SCP 与 SFTP:通过 SSH 传文件
scp:简单快速的文件传输
bash
# 本地 → 远程
scp local_file.txt user@server:/remote/path/
# 远程 → 本地
scp user@server:/remote/path/file.txt ./local/
# 递归传输整个目录
scp -r local_dir/ user@server:/remote/path/
# 指定端口和非默认密钥
scp -P 2222 -i ~/.ssh/work_key file.txt user@server:/path/
sftp:交互式文件管理
sftp 提供类似 FTP 的交互式体验,适合浏览远程目录和选择性传输:
bash
sftp user@server
# 进入 sftp 交互模式后
> ls # 列出远程文件
> cd /data # 切换远程目录
> lcd ~/Downloads # 切换本地目录
> get report.csv # 下载文件
> put backup.sql # 上传文件
> get -r project/ # 递归下载目录
> bye # 退出
如果你的
~/.ssh/config里已经配了 Host 别名,scp和sftp也能直接用别名:scp file.txt work:/data/或sftp vps。
七、安全加固:让你的 SSH 不易被攻破
服务器暴露在公网上,SSH 端口每天会被扫描和暴力破解几百次。以下几项加固措施效果显著且操作简单。
7.1 禁用密码登录,只用密钥
bash
# 编辑 /etc/ssh/sshd_config
PasswordAuthentication no
ChallengeResponseAuthentication no
这一步是最重要的加固措施。没有密码入口,暴力破解就完全失效了。前提是确保你已经配好了密钥登录,否则会把自己锁在外面。
7.2 禁止 root 直接登录
bash
# /etc/ssh/sshd_config
PermitRootLogin no
用普通用户登录后再 sudo 切换,增加一层操作门槛。攻击者即使猜到用户名也无法直接用 root 登录。
7.3 修改默认端口
bash
# /etc/ssh/sshd_config
Port 2222
把 22 改成其他端口不会真正提高安全性(端口扫描很快就能发现),但能大幅减少自动化扫描和暴力破解的日志量,让安全日志更干净。
7.4 部署 Fail2ban
Fail2ban 监控 SSH 登录日志,检测到同一 IP 多次失败后自动封禁:
bash
# 安装
sudo apt install fail2ban
# 创建配置 /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
findtime = 600
同一 IP 在 10 分钟内失败 3 次就封禁 1 小时。简单有效。
7.5 限制可登录 IP
bash
# /etc/ssh/sshd_config
AllowUsers deploy@10.0.1.* admin@192.168.1.50
只允许特定用户从特定 IP 段登录。如果你的服务器只有固定的几个人访问,这是最彻底的防护。
7.6 加固速查表
| 措施 | 防护效果 | 难度 |
|---|---|---|
| 禁用密码登录 | 杜绝暴力破解 | 低 |
| 禁止 root 登录 | 增加攻击门槛 | 低 |
| 修改默认端口 | 减少扫描噪音 | 低 |
| Fail2ban | 自动封禁恶意 IP | 中 |
| IP 白名单 | 仅允许已知来源 | 中 |
| 定期轮换密钥 | 降低密钥泄露风险 | 中 |
修改
sshd_config后记得sudo systemctl restart sshd生效。修改前保持一个已连接的 SSH 会话不关闭,万一新配置有问题还能回滚。
八、常见坑与排查
坑一:Permission denied (publickey)
最常见的密钥认证失败。按优先级排查:
-
文件权限不对 。SSH 对权限极其严格:
~/.ssh目录必须是700,authorized_keys必须是600,私钥必须是600。bashchmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chmod 600 ~/.ssh/id_ed25519 -
服务器没开启公钥认证 。检查
/etc/ssh/sshd_config中PubkeyAuthentication yes。 -
公钥没有正确写入
authorized_keys。重新用ssh-copy-id操作一次。 -
用
-v参数看详细日志定位具体原因:bashssh -v user@server # -vv 或 -vvv 可以看到更详细的调试信息
坑二:Connection timed out / Connection refused
- Connection timed out:网络不通。检查防火墙规则、安全组是否放行了 SSH 端口、服务器 IP 是否正确。
- Connection refused :网络通但 SSH 服务没运行。可能是
sshd没启动,或者端口号不对(你连的是 22 但服务器实际监听 2222)。
坑三:Host key verification failed
服务器密钥和 known_hosts 中的记录不匹配。常见原因:服务器重装了系统、IP 被其他机器占用了、或者你真的遇到了中间人攻击。
bash
# 删除 known_hosts 中对应 IP 的旧记录
ssh-keygen -R 192.168.1.100
# 或者手动编辑 ~/.ssh/known_hosts 删掉对应行
坑四:SSH 连接频繁断开
通常是 NAT 网关或路由器超时清理了空闲的 TCP 连接。解决方案是开启心跳:
bash
# 在 ~/.ssh/config 中全局设置
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
每 60 秒发一个心跳包,连续 3 次无响应才断开(即 3 分钟)。
坑五:Too many authentication failures
SSH 默认最多尝试 6 种认证方式。如果你 ssh-agent 里缓存了太多密钥,还没轮到正确的那个就被拒绝了。
bash
# 指定只用某一个密钥
ssh -i ~/.ssh/specific_key -o IdentitiesOnly=yes user@server
# 或者在 config 里对特定 Host 设置
Host myserver
IdentitiesOnly yes
IdentityFile ~/.ssh/specific_key
九、实用技巧集锦
在远程运行单条命令而不打开 shell
bash
ssh user@server "df -h && free -m"
ssh user@server "docker ps --format '{{.Names}}'"
保持连接不断开(tmux/screen 配合)
SSH 断开后远程进程也会终止。用 tmux 或 screen 可以让进程在后台持续运行:
bash
# SSH 进去后启动 tmux
ssh user@server
tmux new -s train
# 在 tmux 里启动长时间任务
python train.py --epochs 100
# 按 Ctrl+B 然后 D 脱离 tmux
# 断开 SSH 也没关系
# 下次连回来重新接入
ssh user@server
tmux attach -t train
通过 SSH 管道传文件
bash
# 在远程打包并直接下载到本地(不落盘远程)
ssh user@server "tar czf - /data/logs" | tar xzf - -C ./local_logs
# 本地文件直接管道到远程
cat data.csv | ssh user@server "cat > /data/input.csv"
查看 SSH 登录历史
bash
# 查看最近的登录记录
last -n 20
# 查看失败的登录尝试
sudo lastb -n 20
# 查看谁当前在线
who
w
十、总结
SSH 的能力远不止"远程登录"。从底层看,它是一套三层协议(传输层加密 → 认证层验证身份 → 连接层管理通道)。从使用看,密钥认证、ssh_config、端口转发、跳板机、安全加固这五块构成了 SSH 的完整能力栈。
一个实用的自检清单:你的服务器是否已经禁用密码登录?是否部署了 Fail2ban?常用的服务器是否配好了 ssh_config 别名?需要用内网服务时是否知道怎么用端口转发?这些都搞定,SSH 对你来说就不只是"一个登录工具"了。