SSH:安全地登录和控制远程服务器
1. 定义
SSH(Secure Shell) 是一种在不可信网络上安全登录远程机器、执行命令和传输数据的协议及工具。
本地电脑(SSH Client)
│
│ TCP 连接,通常到远端 22 端口
│ 里面承载加密、认证后的 SSH 通道
▼
远程服务器(SSH Server / sshd)它解决三个问题:
- 保密性:旁观者看不到命令、密码和传输内容;
- 完整性:通信内容被篡改时可以被发现;
- 身份认证:客户端确认服务器身份,服务器也确认登录用户身份。
一句话理解:
SSH 是一条经过双向身份检查的加密管道;远程 Shell、文件传输和端口转发,都是这条管道中承载的不同能力。
SSH 不是:
- Shell 本身:
zsh、bash才是 Shell,SSH 只是把远端 Shell 接到本地终端; - VPN:SSH 可以转发端口,但默认不会像 VPN 一样接管整个设备的网络;
- 远程桌面:SSH 主要传文本和网络连接,不直接传完整图形桌面;
- 单纯的“免密登录”:密钥认证只是 SSH 的一种认证方式。
关联:SSH 建立在 TCP 上,端口和连接的原理见
tcp-udp.md;远端运行sshd的角色见server.md。
2. 一次 SSH 连接发生了什么
客户端 服务器
│ │
├──── 1. TCP connect:连接 host:22 ────────────▶│
│ │
├◀─── 2. 服务器出示 Host Key,客户端核验 ───────┤
│ “你真的是我要连接的那台机器吗?” │
│ │
├──── 3. 协商密钥,建立加密通道 ────────────────▶│
│ │
├──── 4. 用户认证:证明我有私钥/知道密码 ───────▶│
│ “我有权登录这个远端账号吗?” │
│ │
├◀═══ 5. 打开 Shell / 执行命令 / 转发端口 ═══════▶│
│ 后续通信全部加密 │最关键的是分清两次身份检查:
| 检查 | 要回答的问题 | 谁持有秘密 | 本地/远端记录 |
|---|---|---|---|
| 服务器认证 | “远端服务器是真的吗?” | 服务器持有 Host Private Key | 本地 ~/.ssh/known_hosts 记住服务器公钥 |
| 用户认证 | “这个用户有登录权限吗?” | 客户端持有 User Private Key | 远端 ~/.ssh/authorized_keys 存放允许的用户公钥 |
这两组密钥用途不同:
- Host Key 属于服务器,用于防止你连到冒牌服务器;
- User Key 属于用户,用于向服务器证明自己的身份;
- 二者都不是每次连接时用来“直接加密所有流量”的会话密钥;
- 实际流量使用握手时临时协商出的对称会话密钥加密。
3. 最基本的连接命令
# 登录:user 是远端系统用户名
ssh user@example.com
# 指定 IP
ssh user@203.0.113.10
# SSH 服务不在默认 22 端口
ssh -p 2222 user@example.com
# 不进入交互式 Shell,只执行一条远端命令
ssh user@example.com 'uname -a'
# 执行需要终端的程序,如 top 或远端 sudo
ssh -t user@example.com 'sudo systemctl status nginx'命令里的身份必须从远端视角理解:
ssh ava@server.example.com
└─┬─┘ └─────────┬─────────┘
远端用户名 远端主机本地电脑叫不叫 ava 并不重要;SSH 要登录的是远端机器上的 ava 账号。
连接卡死时快速断开
在交互式 SSH 会话中,先按一次 Enter,再输入:
~.这是 SSH 客户端的转义序列,会立即断开连接。它只在一行开头被识别。
4. 第一次连接:不要盲目输入 yes
第一次连接某台服务器时通常会看到:
The authenticity of host 'example.com' can't be established.
ED25519 key fingerprint is SHA256:...
Are you sure you want to continue connecting?这表示本地还不认识服务器的 Host Key。正确做法是:
- 从云厂商控制台、服务器管理员或另一个可信渠道获得 Host Key 指纹;
- 对比终端中的
SHA256:...; - 确认一致后再接受;
- SSH 把结果写入
~/.ssh/known_hosts。
如果每次都无条件输入 yes,第一次连接仍可能遭遇中间人攻击。
服务器重装、Host Key 被替换或 DNS 指向了另一台机器时,会出现:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!不要直接删记录。先从可信渠道确认服务器确实更换了密钥,然后才执行:
# 查看已有记录
ssh-keygen -F example.com
# 确认换机/换密钥后,删除旧记录
ssh-keygen -R example.com这个警告既可能是正常换机,也可能是在提醒中间人攻击。
5. 公钥认证:私钥留本地,公钥放服务器
5.1 生成密钥对
新建个人密钥优先使用 Ed25519:
ssh-keygen -t ed25519 -C "ava@personal-mac"默认生成:
~/.ssh/id_ed25519 私钥:只能自己持有,绝不能发送给服务器或他人
~/.ssh/id_ed25519.pub 公钥:可以复制到服务器、GitHub 等服务建议为私钥设置 passphrase。这样即使私钥文件被复制,攻击者仍需要解锁口令。它和服务器账号密码不是一回事:
- 服务器密码由服务器验证;
- 私钥 passphrase 只在本地用于解锁私钥;
- passphrase 不会发送给服务器。
5.2 把公钥安装到服务器
Linux 上有 ssh-copy-id 时:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@example.commacOS 没有 ssh-copy-id 时,可以:
cat ~/.ssh/id_ed25519.pub |
ssh user@example.com 'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'服务器最终保存的是一行公钥:
远端 ~/.ssh/authorized_keys它不是把私钥“上传到服务器”。登录时,服务器发送挑战,客户端使用私钥完成签名;服务器用已登记的公钥验证签名。
5.3 文件权限
OpenSSH 会拒绝使用权限过宽的敏感文件。常用权限:
# 本地
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/config
# 远端
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys权限数字的直觉:
700:只有自己能进入、读取和修改目录;600:只有自己能读写文件;644:自己可写,其他用户只能读,适用于不保密的公钥。
6. ssh-agent:替你暂时保管已解锁的私钥
有 passphrase 的私钥如果每次都输入口令会很麻烦。ssh-agent 可以在内存中保存已解锁的密钥句柄:
# 查看 agent 当前持有哪些身份
ssh-add -l
# 加载密钥
ssh-add ~/.ssh/id_ed25519
# 从 agent 移除所有密钥
ssh-add -DmacOS 可以把 passphrase 放入 Keychain:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519这提高的是使用便利性,不代表私钥可以不设 passphrase。
Agent Forwarding 为什么要谨慎
ssh -A 或 ForwardAgent yes 会让远端机器借用本地 agent 进行认证。远端通常拿不到私钥文件,但如果远端已被入侵,攻击者可能在连接期间借 agent 完成签名,以你的身份继续访问其他机器。
因此:
- 默认不要全局启用
ForwardAgent yes; - 只对确实需要且可信的主机临时启用;
- 进入内网优先使用
ProxyJump; - 高价值密钥可使用硬件安全密钥,并要求触摸确认。
7. ~/.ssh/config:把长命令变成短别名
不想每次输入:
ssh -p 2222 -i ~/.ssh/work_ed25519 deploy@203.0.113.10可以写入 ~/.ssh/config:
Host prod
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/work_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
ServerAliveInterval 60
ServerAliveCountMax 3以后只需:
ssh prod
scp ./app.tar.gz prod:/tmp/常用字段:
| 配置 | 含义 |
|---|---|
Host | 本地别名或匹配模式,不一定是真实域名 |
HostName | 真实域名或 IP |
User | 远端用户名 |
Port | SSH 服务端口 |
IdentityFile | 为该主机使用的私钥 |
IdentitiesOnly yes | 只尝试明确指定的密钥,避免 agent 乱试 |
ProxyJump | 通过跳板机访问目标 |
ServerAliveInterval | 空闲时定期发送应用层保活消息 |
LocalForward | 固化本地端口转发 |
macOS 使用 Keychain 时可增加:
Host *
AddKeysToAgent yes
UseKeychain yes配置匹配时,OpenSSH 通常采用每个参数最先得到的值,所以更具体的 Host prod 应写在通用的 Host * 前面。
检查某个别名最终生效了什么配置:
ssh -G prod多个 GitHub 身份
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/personal_ed25519
IdentitiesOnly yes
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/work_ed25519
IdentitiesOnly yesGit remote 使用别名:
git clone git@github-work:company/project.git这里 github-work 只是本地 SSH 别名,真正连接的仍是 github.com。
8. 文件传输:SFTP、SCP 与 rsync
SCP
# 本地 → 远端
scp ./report.pdf user@example.com:/home/user/
# 远端 → 本地
scp user@example.com:/var/log/app.log ./
# 复制目录
scp -r ./dist user@example.com:/srv/app/
# 注意:scp 的端口参数是大写 -P
scp -P 2222 ./file.txt user@example.com:/tmp/现代 OpenSSH 的 scp 默认通过 SFTP 协议传输;命令界面仍保留熟悉的 scp 形式。
SFTP
sftp user@example.com
sftp> ls
sftp> pwd
sftp> lpwd
sftp> get remote.txt
sftp> put local.txt
sftp> exitpwd/ls 操作远端,带 l 的 lpwd/lls 操作本地。
rsync
大量文件或需要增量同步时,rsync 通常比反复 scp -r 更合适:
rsync -av --progress ./dist/ user@example.com:/srv/app/
# 指定 SSH 端口
rsync -av -e 'ssh -p 2222' ./dist/ user@example.com:/srv/app/注意末尾 /:
./dist/ 复制 dist 里面的内容
./dist 复制 dist 目录本身9. 端口转发:借 SSH 加密通道访问其他服务
9.1 本地转发 -L
场景:远端 PostgreSQL 只监听 127.0.0.1:5432,公网不能直接访问。
ssh -N \
-L 127.0.0.1:5433:127.0.0.1:5432 \
user@example.com数据路径:
本地程序 → 本地 127.0.0.1:5433
→ SSH 加密通道
→ 从服务器侧访问 127.0.0.1:5432然后本地数据库客户端连接 127.0.0.1:5433。
需要特别理解:-L 最后的目标地址是从远端服务器一侧解释的;其中的 127.0.0.1 指远端服务器,不是本机。
9.2 远程转发 -R
场景:让远端服务器通过一个端口访问本机开发服务:
ssh -N \
-R 127.0.0.1:8080:127.0.0.1:3000 \
user@example.com远端 127.0.0.1:8080
→ SSH 加密通道
→ 本地 127.0.0.1:3000默认只绑定远端 loopback 比较安全。若要公开给其他机器,还涉及服务器的 GatewayPorts、防火墙和额外认证,不应只把监听地址改成 0.0.0.0 就直接暴露。
9.3 动态转发 -D
ssh -N -D 127.0.0.1:1080 user@example.comSSH 会在本地建立一个 SOCKS 代理。支持 SOCKS 的应用可把代理指向 127.0.0.1:1080,由服务器代为建立目标连接。
常配参数
ssh -N -T \
-o ExitOnForwardFailure=yes \
-L 127.0.0.1:5433:127.0.0.1:5432 \
prod-N:不执行远端命令,只建通道;-T:不分配伪终端;ExitOnForwardFailure=yes:端口绑定失败时直接报错退出。
10. 跳板机:ProxyJump
目标服务器没有公网入口,只允许通过堡垒机访问:
本机 ──SSH──▶ bastion ──TCP forwarding──▶ private-server一次性命令:
ssh -J ops@bastion.example.com deploy@10.0.1.20配置方式:
Host bastion
HostName bastion.example.com
User ops
IdentityFile ~/.ssh/bastion_ed25519
Host private-app
HostName 10.0.1.20
User deploy
IdentityFile ~/.ssh/app_ed25519
IdentitiesOnly yes
ProxyJump bastionssh private-appProxyJump 通常比 Agent Forwarding 更安全:连接由本地 SSH 客户端端到端处理,跳板机只转发字节,不需要借用本地 agent 去登录下一跳。
11. 服务器端加固
服务器配置通常位于:
/etc/ssh/sshd_config一份常见的起点:
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers deploy但不要直接照抄后重启。安全操作顺序:
- 先创建普通管理账号并配置
sudo; - 安装并测试公钥登录;
- 保持当前 SSH 会话不要关闭;
- 修改
sshd_config; - 运行配置检查;
- reload 服务;
- 从另一个新终端测试成功;
- 最后才关闭旧会话。
# 检查服务端配置语法
sudo sshd -t
# 不同 Linux 发行版服务名可能不同
sudo systemctl reload sshd
# 或
sudo systemctl reload ssh否则一个拼写错误或错误的认证设置就可能把自己锁在服务器外。
其他原则:
- 防火墙或云安全组只向必要来源开放 SSH;
- 优先使用密钥认证,并为私钥设置 passphrase;
- 及时更新 OpenSSH 和操作系统;
- 为不同设备、人员和用途使用不同密钥,方便单独撤销;
- 定期清理
authorized_keys中不再使用的公钥; - 高风险环境使用硬件密钥、MFA 或 SSH Certificates;
- 改到非 22 端口只能减少日志噪声,不是实质安全边界;
- 不要为了兼容旧机器随意重新启用已淘汰算法。
12. 排障:从网络到认证逐层检查
12.1 通用排障顺序
DNS 能解析吗?
↓
TCP 端口能连接吗?
↓
SSH 握手和 Host Key 正常吗?
↓
客户端实际选择了哪份配置/哪把密钥?
↓
服务器是否允许该用户和公钥?
↓
远端文件权限和 sshd 日志说了什么?对应命令:
# 1. DNS
dig +short example.com
# 2. TCP 22 端口
nc -vz example.com 22
# 3. SSH 调试;v 越多日志越详细,最多常用到 -vvv
ssh -vvv user@example.com
# 4. 查看最终配置
ssh -G prod
# 5. agent 中的密钥
ssh-add -l
# 6. 检查服务器 Host Key 记录
ssh-keygen -F example.com12.2 常见报错
| 报错 | 常见原因 | 优先检查 |
|---|---|---|
Connection timed out | 网络不通、防火墙丢包、安全组没开 | IP、路由、安全组、nc -vz |
Connection refused | 主机可达,但端口无人监听或被主动拒绝 | sshd 是否运行、端口是否写错 |
No route to host | 本机没有到目标网段的可用路由 | VPN、网关、目标 IP |
Permission denied (publickey) | 用户名/公钥/私钥不匹配或远端权限错误 | User、IdentityFile、authorized_keys |
Too many authentication failures | agent 自动尝试了太多密钥 | IdentitiesOnly yes + 指定 IdentityFile |
Host key verification failed | Host Key 未信任或发生变化 | 核验指纹,不要直接关闭检查 |
REMOTE HOST IDENTIFICATION HAS CHANGED | 正常换机或中间人攻击 | 先可信核验,再 ssh-keygen -R |
Bad permissions | 私钥或 .ssh 权限过宽 | chmod 700/600 |
Broken pipe | NAT/防火墙回收空闲连接、网络切换 | ServerAliveInterval、检查网络 |
12.3 Permission denied (publickey) 的四个高频原因
- 远端用户名错了:云服务器可能使用
ubuntu、ec2-user、debian等; - 客户端选错私钥:用
ssh -vvv看Offering public key; - 公钥没放进目标用户的
authorized_keys; - 远端目录或文件权限/owner 错误。
不要通过把所有文件 chmod 777 来“解决权限问题”。这会让 SSH 更可能拒绝使用文件,同时制造新的安全漏洞。
13. 一张速查表
| 目的 | 命令 |
|---|---|
| 登录 | ssh user@host |
| 指定端口 | ssh -p 2222 user@host |
| 指定私钥 | ssh -i ~/.ssh/key user@host |
| 执行远端命令 | ssh host 'command' |
| 生成 Ed25519 密钥 | ssh-keygen -t ed25519 -C "device-name" |
| 查看 agent 密钥 | ssh-add -l |
| 查看最终配置 | ssh -G alias |
| 调试连接 | ssh -vvv alias |
| 本地转发 | ssh -N -L 127.0.0.1:本地端口:目标:目标端口 host |
| 远程转发 | ssh -N -R 127.0.0.1:远端端口:目标:目标端口 host |
| SOCKS 代理 | ssh -N -D 127.0.0.1:1080 host |
| 跳板机 | ssh -J jump-host target-host |
| 上传文件 | scp file host:/path/ |
| 下载文件 | scp host:/path/file ./ |
| 卡死时断开 | 换到新行后输入 ~. |
14. 常见误区
-
❌ “公钥和私钥都要上传服务器”
→ 服务器只需要公钥;私钥应留在受控客户端。 -
❌ “SSH 不问服务器密码就是没有密码”
→ 可能使用私钥认证;私钥本身还应有本地 passphrase。 -
❌ “接受过 Host Key,以后变化直接删掉就行”
→ Host Key 变化也可能意味着中间人攻击,必须先核验。 -
❌ “禁用密码登录后绝对安全”
→ 私钥泄漏、服务器漏洞、过宽权限和供应链攻击仍然存在。 -
❌ “把 SSH 改成 2222 就安全了”
→ 只能减少自动扫描噪声,扫描全部端口仍能发现服务。 -
❌ “
ssh -A很方便,可以全局开启”
→ 被入侵的远端主机可能借用 agent 进行身份认证。 -
❌ “
scp -r适合所有同步”
→ 大目录、断点续传和增量更新更适合rsync。 -
❌ “SSH tunnel 就是 VPN”
→ tunnel 默认只转发明确指定的端口;VPN 通常处理整个网段或系统流量。
15. 延伸阅读与关联概念
tcp-udp.md— SSH 为什么先建立 TCP 连接;server.md— 远端sshd为什么是一种监听 22 端口的服务器程序;socket.md— SSH 连接和端口转发底层使用的通信端点;password-auth.md— 密码认证与公钥认证的差异;gateway.md— 跳板机、内网入口和网络出口;network-security.md— 最小权限、暴露端口和中间人攻击;vps-vpn.md— SSH 端口转发与 VPN 的边界;../git/01-clone空仓库首次push流程.md— 通过 SSH 或 HTTPS remote 完成首次 push 的 Git 工作流;../command/tmux-ghostty-workflow.md— SSH 断开后,如何用 tmux 让远端任务继续运行。