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 2048 | 2048 bit | 较慢 | 接近安全边界 | 所有系统 |
| RSA 4096 | 4096 bit | 慢 | 强 | 所有系统 |
| ed25519 | 256 bit | 极快 | 强(等价 RSA 3072+) | OpenSSH 6.5+ |
ed25519 密钥更短、签名更快、安全性更高,且签名是确定性的,避免了对随机数生成器的依赖。除非要兼容特别旧的系统,新的个人密钥优先用 ed25519。
密钥文件与权限
| 文件 | 权限 | 说明 |
|---|---|---|
~/.ssh/id_ed25519 | 600 | 私钥,绝对不能泄露 |
~/.ssh/id_ed25519.pub | 644 | 公钥,可以公开 |
~/.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_ed255194. 登录配置:方便不能替代边界
所谓“免密登录”,原理不是取消认证,而是把认证从“输入密码”换成“证明你持有私钥”:公钥放服务器 ~/.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 -DmacOS Keychain 集成
ssh-add --apple-use-keychain ~/.ssh/id_ed25519在 ~/.ssh/config 中添加:
Host *
AddKeysToAgent yes
UseKeychain yesAgent 转发
通过跳板机连接内网时,-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 myserver、Host *.example.com |
HostName | 实际主机名或 IP | HostName 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-server7. 端口转发:把网络边界显式化
端口转发通过 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 复制目录本身。
对比
| 特性 | scp | rsync |
|---|---|---|
| 增量传输 | ❌ | ✅ 只传差异 |
| 断点续传 | ❌ | ✅ --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 refused | sshd 未运行、防火墙阻拦 | 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 最重要的不是“免密”,而是把远程访问变成一组可解释、可撤销、可审计的信任关系。
延伸阅读
- SSH.com Academy — SSH 协议详解
- GitHub SSH 文档 — GitHub SSH 配置
- DigitalOcean SSH 参考 — 实用参考
man ssh_config— 客户端配置手册man sshd_config— 服务端配置手册- RFC 4251 — SSH 协议规范