SSH 是开发者进入远程世界的信任边界。连接服务器、推送代码、访问内网服务、让 Agent 在远程环境里执行命令,最后都绕不开它。SSH 配得好,不只是少输密码,而是知道自己在信任哪台机器、哪把密钥、哪条隧道,以及出了问题该从哪里切开。

1. SSH 是什么

SSH(Secure Shell)是一种加密的网络协议,用于在不安全的网络上安全地运行远程命令。它诞生于 1995 年,目的是替代当时完全不加密的 Telnet 和 FTP 协议。

今天看 SSH,它已经不只是“远程登录工具”。它同时承担身份认证、远程执行、Git 传输、端口转发、跳板机访问、文件同步和开发环境入口。很多 2026 年常见工作流,比如远程开发、临时云主机、CI 部署、Agent 连接服务器,本质上都在复用 SSH 的信任模型。

“安全”在 SSH 中意味着三件事:

安全维度含义对比 Telnet
加密所有流量使用对称加密传输Telnet 明文传输,抓包即可看到密码
认证通过密钥或密码验证身份Telnet 无强认证机制
完整性数据在传输中不被篡改Telnet 数据可被中间人修改

SSH 采用客户端 - 服务端模型:你的机器运行 ssh 客户端,远程机器运行 sshd 守护进程,默认监听端口 22。日常命令很短,但背后要确认三件事:连到谁、以谁的身份连、用哪把密钥证明身份。

# 基本连接
ssh [email protected]
 
# 指定端口和密钥
ssh -p 2222 -i ~/.ssh/my_key [email protected]

2. 加密基础:对称与非对称

SSH 的安全性建立在两种加密方式的配合上。

对称加密: 加密和解密使用同一个密钥。速度快,适合大量数据传输。

非对称加密: 有一对密钥——公钥和私钥。公钥可以公开,私钥必须保密。速度慢,但解决了密钥分发问题。

SSH 用非对称加密完成身份认证和密钥交换,之后用对称加密传输实际数据。

SSH 握手过程

流程图:
 
客户端                    服务端
  |                        |
  | --- TCP 连接 (端口 22) --> |
  | <-- 发送 Host Key ------|  (服务端身份标识)
  | 检查 known_hosts        |  (首次连接会询问)
  | --- 密钥交换 (DH) ------> |  (协商共享对称密钥)
  | <-- 发送认证挑战 --------|
  | --- 用私钥签名挑战 -----> |  (证明你拥有私钥)
  | <-- 用公钥验证签名 ------|  (对比 authorized_keys)
  | === 会话建立 =========== |  (后续用对称加密通信)

首次连接时你会看到:

The authenticity of host '192.168.1.100' can't be established.
ED25519 key fingerprint is SHA256:xxxxx.
Are you sure you want to continue connecting (yes/no)?

这是 SSH 防止**中间人攻击(MITM)**的机制。第一次连接不是一个可以无脑敲 yes 的仪式,而是在把“这个地址对应这台机器”的事实写进 known_hosts。如果生产环境指纹变了,要先确认机器是否重装、IP 是否复用、DNS 是否被污染,再决定是否清理旧记录。

可以把握手理解成两层确认:

  • 确认服务端:服务端发送 Host Key,客户端检查 ~/.ssh/known_hosts,避免自己连到假机器。
  • 确认客户端:服务端发送随机挑战,客户端用私钥签名,服务端用 authorized_keys 中的公钥验证,避免陌生人冒充你登录。

后续通信再用协商出的对称密钥加密。真正要记住的是:Host Key 保护你不连错机器,用户密钥保护服务器不放错人。

3. 密钥对生成与管理

生成密钥对

ssh-keygen -t ed25519 -C "[email protected]"

为什么常用 ed25519? 基于椭圆曲线的现代算法,相比 RSA 有明显优势:

算法密钥长度签名速度安全性兼容性
RSA 20482048 bit较慢接近安全边界所有系统
RSA 40964096 bit所有系统
ed25519256 bit极快强(等价 RSA 3072+)OpenSSH 6.5+

ed25519 密钥更短、签名更快、安全性更高,且签名是确定性的,避免了对随机数生成器的依赖。除非要兼容特别旧的系统,新的个人密钥优先用 ed25519。

密钥文件与权限

文件权限说明
~/.ssh/id_ed25519600私钥,绝对不能泄露
~/.ssh/id_ed25519.pub644公钥,可以公开
~/.ssh/ 目录700目录权限也必须正确

权限不对 SSH 会直接拒绝:

WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions 0644 for '/home/user/.ssh/id_ed25519' are too open.

修复:chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519

密码短语与多密钥

生成密钥时建议设置 passphrase,相当于给私钥加了一层加密。配合 ssh-agent(见第 5 节),只需输入一次。

为不同服务使用不同密钥是良好的安全实践。这样某把密钥泄露时,可以只撤销对应服务,而不是重建全部远程身份:

ssh-keygen -t ed25519 -C "github" -f ~/.ssh/gh_ed25519
ssh-keygen -t ed25519 -C "server" -f ~/.ssh/server_ed25519

4. 登录配置:方便不能替代边界

所谓“免密登录”,原理不是取消认证,而是把认证从“输入密码”换成“证明你持有私钥”:公钥放服务器 ~/.ssh/authorized_keys,私钥留在本地。这个模型的安全性取决于私钥是否受保护、服务器是否只接受正确用户、Host Key 是否可信。

ssh-copy-id

ssh-copy-id [email protected]

ssh-copy-id 会读取本地公钥,登录服务器,创建 .ssh 目录,追加到 authorized_keys 并设置权限。适合你确实拥有目标账号密码、目标机器可信、只是想把密码登录切换到密钥登录的场景。

手动复制

cat ~/.ssh/id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

手动复制适合受限环境或临时机器。注意不要把私钥内容复制到服务器;服务器只需要 .pub 公钥。

GitHub / GitLab

复制公钥 cat ~/.ssh/id_ed25519.pub,粘贴到 GitHub → Settings → SSH and GPG keys → New SSH key。测试连接:

ssh -T [email protected]
# Hi yourname! You've successfully authenticated...

服务端配置

确保 /etc/ssh/sshd_config 中:

PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

修改后 sudo systemctl restart sshd。生产服务器修改 sshd_config 时,保留一个已登录会话再重启服务,避免配置写错把自己锁在门外。

5. SSH Agent

ssh-agent 在内存中保存解密后的私钥,解决 passphrase 的便利性问题:会话开始时输入一次,之后自动使用。它让密钥更方便,也扩大了会话期间的可用范围,所以要知道自己把哪些密钥加载进了 agent。

# 启动 agent
eval "$(ssh-agent -s)"
 
# 添加密钥(输入 passphrase)
ssh-add ~/.ssh/id_ed25519
 
# 查看已加载密钥
ssh-add -l
 
# 删除所有密钥
ssh-add -D

macOS Keychain 集成

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

~/.ssh/config 中添加:

Host *
    AddKeysToAgent yes
    UseKeychain yes

Agent 转发

通过跳板机连接内网时,-A 让内网服务器临时使用本地 agent:

ssh -A user@bastion
ssh user@internal-server  # 使用本地密钥认证

安全警告: 跳板机的 root 用户可以临时借用你的 agent 去连接其他机器。Agent Forwarding 只应该在可信机器上短时间启用。更稳的默认选择是 ProxyJump:连接路径经过跳板机,但私钥仍留在本地。

6. ~/.ssh/config 配置文件

~/.ssh/config 让你给主机起别名、设置默认参数、配置跳板机,把冗长命令简化为一个稳定入口。它的价值不是“少打字”,而是把主机名、用户、端口、密钥和跳板路径固化成可审计的配置。

配置示例

# GitHub - 专用密钥
Host github
    HostName github.com
    User git
    IdentityFile ~/.ssh/gh_ed25519
    IdentitiesOnly yes
 
# 个人服务器 - 自定义端口
Host myserver
    HostName 192.168.1.100
    User root
    Port 2222
    IdentityFile ~/.ssh/server_ed25519
    IdentitiesOnly yes
 
# 工作服务器 - 通配符
Host *.example.com
    User admin
    IdentityFile ~/.ssh/work_ed25519
    IdentitiesOnly yes
 
# 跳板机
Host bastion
    HostName jump.example.com
    User zopiya
    IdentityFile ~/.ssh/work_ed25519
 
# 内网服务器 - 通过跳板机
Host internal-server
    HostName 10.0.1.50
    User deploy
    ProxyJump bastion
    IdentityFile ~/.ssh/work_ed25519

关键指令

指令作用示例
Host别名(支持通配符)Host myserverHost *.example.com
HostName实际主机名或 IPHostName 192.168.1.100
User默认用户名User root
Port非默认端口Port 2222
IdentityFile私钥路径IdentityFile ~/.ssh/gh_ed25519
IdentitiesOnly只用指定密钥IdentitiesOnly yes
ProxyJump通过跳板机ProxyJump bastion
ServerAliveInterval保持连接(秒)ServerAliveInterval 60

效果对比

# 没有 config
 
ssh [email protected] -p 2222 -i ~/.ssh/server_ed25519
 
# 有了 config
 
ssh myserver
 
# 内网直达(自动经过跳板机)
 
ssh internal-server

7. 端口转发:把网络边界显式化

端口转发通过 SSH 建立加密隧道,访问原本不可达的服务。它不是魔法,核心只是把“谁能访问哪个端口”改写成一条临时、可关闭、可审计的路径。用之前先问清楚:服务本来在哪台机器上、谁应该访问、暴露范围有多大、隧道断开后是否会留下风险。

本地转发(-L):把远程服务接到本地

语法: ssh -L 本地端口:目标主机:目标端口 user@跳板机

数据流向: 本地应用 → 本地端口 → SSH 隧道 → 跳板机 → 目标主机:目标端口

# 远程数据库只监听 localhost
ssh -L 5432:localhost:5432 user@dbserver
psql -h localhost -p 5432 -U postgres  # 就像数据库在本地
 
# 通过跳板机访问内网 Web
ssh -L 8080:internal.example.com:80 user@bastion
# 浏览器访问 http://localhost:8080

-fN 可以后台运行且不打开 shell:ssh -fN -L 5432:localhost:5432 user@dbserver。后台隧道方便,也容易忘记;长期使用时最好让命令可见,或者把隧道交给专门的任务入口管理。

远程转发(-R):把本地服务暴露给远程

语法: ssh -R 远程端口:本地主机:本地端口 user@服务器

数据流向: 远程用户 → 远程端口 → SSH 隧道 → 本地主机:本地端口

# 让同事访问你本地的 dev server
 
ssh -R 8080:localhost:3000 user@shared-server
# 同事访问 shared-server:8080 即可

远程转发风险更高,因为它会把本地端口提供给远程侧。临时演示、协作调试可以用;生产访问和长期暴露应改用正式的网关、VPN、反向代理或部署流程。

动态转发(-D):SOCKS5 代理

语法: ssh -D 本地端口 user@服务器

ssh -D 1080 user@server
# 浏览器设置 SOCKS5 代理 localhost:1080
# 所有流量通过 SSH 隧道转发

三种转发对比

类型参数方向典型场景
本地转发-L本地 → 远程访问远程数据库、内网服务
远程转发-R远程 → 本地暴露本地服务给他人
动态转发-D本地 ↔ 任意SOCKS5 代理,安全浏览

8. 文件传输:一次性复制与可重复同步

scp:简单复制

# 本地 → 远程
 
scp file.txt user@host:/path/
scp -r ./dir user@host:/path/
 
# 远程 → 本地
 
scp user@host:/remote/file.txt ./local/
 
# 指定端口
 
scp -P 2222 file.txt user@host:/path/

rsync:增量同步

# 基本同步(-a 归档,-v 详细,-z 压缩)
rsync -avz ./local/ user@host:/remote/
 
# 部署网站(--delete 保持完全一致)
rsync -avz --delete ./dist/ user@host:/var/www/html/
 
# 预览模式(不实际传输)
rsync -avz --dry-run ./local/ user@host:/remote/
 
# 排除文件
rsync -avz --exclude='node_modules/' ./project/ user@host:/remote/

注意路径末尾斜杠: ./local/ 复制目录内容,./local 复制目录本身。

对比

特性scprsync
增量传输✅ 只传差异
断点续传--partial
文件过滤--exclude
删除同步--delete
速度(重复)不变大幅加快

经验法则: 一次性复制用 scp,重复同步用 rsync。如果这个传输动作会反复发生,就不要只留在终端历史里;把它写进脚本、justfile 或部署流水线,并明确是否允许 --delete

9. 常见问题排查

错误诊断表

错误信息原因解决方案
Permission denied (publickey)私钥权限不对、公钥未部署、sshd 未启用chmod 600 私钥;检查 authorized_keys;确认 PubkeyAuthentication yes
Host key verification failed服务器重装或 IP 变更ssh-keygen -R hostname 移除旧记录
Connection refusedsshd 未运行、防火墙阻拦sudo systemctl status sshd;检查端口和防火墙
Connection timed out网络不通、IP 错误ping host 测试连通性
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED服务器重装或中间人攻击先确认安全! 确认后 ssh-keygen -R hostname
Too many authentication failures尝试了太多密钥-o IdentitiesOnly=yes 指定密钥

调试模式

ssh -vvv user@host  # 最详细输出

-vvv 显示读取的配置文件、尝试的密钥、算法协商过程、认证步骤和失败位置。排查 SSH 时不要先猜密码错了,应该按顺序看:配置是否命中、主机是否可达、Host Key 是否接受、尝试了哪把密钥、服务端为什么拒绝。

10. 安全最佳实践

服务器端

# /etc/ssh/sshd_config
PasswordAuthentication no      # 禁用密码认证
PermitRootLogin no             # 禁用 root 登录
AllowUsers zopiya deploy       # 限制允许登录的用户
MaxAuthTries 3                 # 限制认证尝试
ClientAliveInterval 300        # 空闲超时
措施作用
fail2ban自动封禁暴力破解 IP
更改默认端口减少扫描噪音(非安全措施)
定期审计日志发现异常登录 journalctl -u sshd
密钥轮换定期更换降低泄露风险

客户端

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/config

安全检查清单

  • 使用 ed25519 密钥
  • 私钥设置了 passphrase
  • 私钥权限 600,~/.ssh 目录 700
  • 服务器禁用密码认证和 root 登录
  • 为不同服务使用不同密钥
  • 不随意启用 Agent Forwarding
  • 首次连接时验证主机指纹

11. 我的使用原则

SSH 的命令很多,但真正要长期保持的是几条边界意识。

私钥只留在本地可信环境:服务器、跳板机、临时容器和陌生项目目录都不应该保存我的私钥。需要跨机器访问时,优先用 ProxyJump、单独密钥或服务端授权,而不是复制私钥。

Host Key 变化要停一下WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED 不只是烦人的报错。确认机器重装、IP 复用或 DNS 变化之前,不要直接清掉 known_hosts

一把密钥只承担一类身份:GitHub、个人服务器、公司环境、临时云主机尽量分开。这样撤销和轮换才有边界。

隧道是临时边界,不是长期架构-L-R-D 适合调试、排障和临时访问。只要一个隧道开始长期存在,就应该考虑正式网关、VPN、反向代理或部署流程。

把常用连接写进配置~/.ssh/config 是个人远程环境的路由表。别名、用户、端口、密钥和跳板机都写清楚,未来的自己、脚本和 Agent 才能稳定复用。

SSH 最重要的不是“免密”,而是把远程访问变成一组可解释、可撤销、可审计的信任关系。

延伸阅读