连接服务器、推送代码到 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_ed255193. 配置公钥
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_config 里 PubkeyAuthentication 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 yesAgent 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 refused | sshd 未运行 / 防火墙阻拦 | sudo systemctl status sshd;检查端口和防火墙 |
Too many authentication failures | agent 逐个尝试了太多密钥 | 配置文件加 IdentitiesOnly yes |
加 -vvv 获取最详细输出,排查时先看:配置是否命中、主机是否可达、Host Key 是否接受、尝试了哪把密钥、服务端为什么拒绝。
ssh -vvv user@host9. 安全清单
服务器端 /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、社会工程防范——见 安全实践。