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

1. SSH 是什么

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

“安全”在 SSH 里意味着三件事:加密(所有流量密文传输,Telnet 是明文,抓包即见密码)、认证(通过密钥或密码验证身份)、完整性(数据在传输中不被篡改)。

SSH 是客户端—服务端模型:你的机器跑 ssh 客户端,远程机器跑 sshd,默认监听端口 22。

握手模型

握手做两件事:确认你连的是正确的机器,机器确认你是正确的人。

客户端                               服务端
  |--- TCP 连接 (22) -------------->|
  |<-- 发送 Host Key ---------------|   服务端身份证明
  | 检查 ~/.ssh/known_hosts          |   防止连到假机器
  |--- 密钥交换,协商对称密钥 ------>|
  |<-- 发送认证挑战 ----------------|
  |--- 用私钥签名挑战 ------------->|   证明你持有私钥
  |<-- 用 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)?

这不是可以无脑敲 yes 的仪式。敲 yes 是在把”这个地址对应这台机器”写进 known_hosts。如果后来看到 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED,要先确认机器是否重装、IP 是否复用、DNS 是否被污染,确认安全后再清理旧记录,不能直接跳过。

2. 密钥对:生成与管理

生成密钥

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

ed25519 是目前推荐的算法:密钥短(256 bit)、签名快、安全性高(等价 RSA 3072+)、且签名是确定性的,不依赖随机数生成器的质量。除非要对接极旧的系统(OpenSSH 6.5 以前),新密钥都用 ed25519。

文件权限

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519        # 私钥,绝不外传
chmod 644 ~/.ssh/id_ed25519.pub    # 公钥,可以公开

权限不对 SSH 会直接报错拒绝使用:WARNING: UNPROTECTED PRIVATE KEY FILE!。修复就是 chmod 600 ~/.ssh/id_ed25519

passphrase 与多密钥

生成时建议设置 passphrase——它给私钥文件加了一层加密,即使私钥文件泄露,没有 passphrase 也无法使用。配合 ssh-agent(见下节),只需输入一次。

为不同服务使用不同密钥是好习惯:某把密钥泄露时,只需撤销对应服务的访问,而不是重建全部远程身份。

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

3. 配置公钥

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...

服务器免密登录

ssh-copy-id [email protected]

ssh-copy-id 自动读取本地公钥,登录服务器后追加到 ~/.ssh/authorized_keys 并设置正确权限。服务器端确认 /etc/ssh/sshd_configPubkeyAuthentication yes 已启用。

手动方式:

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

注意:只复制 .pub 公钥到服务器,私钥永远不离开本地。

4. ssh-agent

ssh-agent 在内存中保存解密后的私钥,会话开始输入一次 passphrase,之后自动使用。

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

macOS 可以与 Keychain 集成,重启后不需要重新输入:

# ~/.ssh/config
Host *
    AddKeysToAgent yes
    UseKeychain yes

Agent Forwarding 的安全边界

ssh -A user@bastion 会把本地 agent 转发给跳板机,允许从跳板机继续用本地密钥连接内网。便利,但跳板机的 root 用户在 Forwarding 期间可以临时借用你的 agent 去连接任何授权了你公钥的机器。

更安全的替代方案是 ProxyJump(见下节):连接路径经过跳板机,但私钥始终留在本地 agent。Agent Forwarding 只在可信机器上短时需要时启用。

5. ~/.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 bastion
    HostName jump.example.com
    User zopiya
    IdentityFile ~/.ssh/work_ed25519
 
# 内网服务器 - 通过跳板机直达,私钥不离本地
Host internal
    HostName 10.0.1.50
    User deploy
    ProxyJump bastion
    IdentityFile ~/.ssh/work_ed25519
 
# 全局默认:保持连接
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

配置后效果:

ssh myserver          # 等价于 ssh [email protected] -p 2222 -i ~/.ssh/server_ed25519
ssh internal          # 自动经过跳板机,无需手动 -J

关键指令:IdentitiesOnly yes 确保只使用指定密钥,不让 agent 逐个尝试所有已加载密钥(这会触发 Too many authentication failures)。

6. 端口转发

端口转发通过 SSH 建立加密隧道,访问原本不可达的服务。核心是把”谁能访问哪个端口”改写成一条临时、可关闭、可审计的路径。

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

# 远程数据库只监听 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

-fN 后台运行(不打开 shell):ssh -fN -L 5432:localhost:5432 user@dbserver

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

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

远程转发风险更高,它把本地端口提供给了远程一侧。临时演示和协作调试可以用,长期暴露应改用正式的网关或反向代理。

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

ssh -D 1080 user@server
# 浏览器设置 SOCKS5 代理 localhost:1080,所有流量走隧道
类型参数方向典型场景
本地转发-L本地 → 远程访问远程数据库、内网服务
远程转发-R远程 → 本地临时暴露本地服务给他人
动态转发-D本地 ↔ 任意SOCKS5 代理

7. 文件传输

scp vs rsync

scp:一次性复制,简单直接。

scp file.txt user@host:/path/
scp -r ./dir user@host:/path/       # 递归
scp user@host:/remote/file.txt ./   # 下载
scp -P 2222 file.txt user@host:/path/   # 指定端口

rsync:增量同步,只传差异,断点续传,可过滤文件。

rsync -avz ./local/ user@host:/remote/           # 同步内容
rsync -avz --delete ./dist/ user@host:/var/www/  # 部署(保持完全一致)
rsync -avz --dry-run ./local/ user@host:/remote/ # 预览(不实际传输)
rsync -avz --exclude='node_modules/' ./proj/ user@host:/remote/

注意路径末尾斜杠:./local/ 复制目录内容,./local 复制目录本身,差一个斜杠结果不同。

经验法则:一次性复制用 scp,重复同步用 rsync。同步动作会反复发生时,不要只留在终端历史里,写进脚本或部署流水线,并明确是否启用 --delete

8. 常见报错排查

错误信息原因解决
Permission denied (publickey)私钥权限错 / 公钥未部署 / sshd 未启用公钥认证chmod 600 私钥;检查 authorized_keys;确认 PubkeyAuthentication yes
Host key verification failed服务器重装或 IP 变更ssh-keygen -R hostname 移除旧记录
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED服务器重装或中间人攻击先确认安全,确认后再清理
Connection refusedsshd 未运行 / 防火墙阻拦sudo systemctl status sshd;检查端口和防火墙
Too many authentication failuresagent 逐个尝试了太多密钥配置文件加 IdentitiesOnly yes

-vvv 获取最详细输出,排查时先看:配置是否命中、主机是否可达、Host Key 是否接受、尝试了哪把密钥、服务端为什么拒绝。

ssh -vvv user@host

9. 安全清单

服务器端 /etc/ssh/sshd_config

PasswordAuthentication no     # 禁用密码认证,强制密钥
PermitRootLogin no            # 禁用 root 直接登录
AllowUsers zopiya deploy      # 限制允许登录的用户
MaxAuthTries 3                # 限制认证尝试次数
ClientAliveInterval 300       # 空闲超时

修改 sshd_config 时,保留一个已登录的会话再重启服务——配置写错会把自己锁在门外。

客户端习惯

  • 使用 ed25519 密钥,设置 passphrase
  • 私钥权限 600,~/.ssh 目录 700
  • 不同服务用不同密钥,便于独立撤销
  • 不随意启用 Agent Forwarding,优先用 ProxyJump
  • 首次连接时验证主机指纹,而不是无脑 yes
  • 常用连接写进 ~/.ssh/config,不散落在历史命令里

10. 使用原则

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

Host Key 变化要停一下:看到 REMOTE HOST IDENTIFICATION HAS CHANGED,先确认机器是否重装、IP 是否复用、DNS 是否被污染,再决定是否清理记录——这是防中间人攻击的最后一道关卡。

一把密钥只承担一类身份:GitHub、个人服务器、公司环境尽量分开,撤销和轮换才有干净的边界。

隧道是临时手段-L/-R/-D 适合调试和临时访问。一条隧道开始长期存在,就该换成正式的网关、VPN 或反向代理。

把常用连接写进 config~/.ssh/config 是个人远程环境的路由表,未来的你和 Agent 都依赖它稳定工作。

SSH 之外更广泛的安全实践——凭证管理、依赖扫描、2FA、社会工程防范——见 安全实践