SSH:安全地登录和控制远程服务器

1. 定义

SSH(Secure Shell) 是一种在不可信网络上安全登录远程机器、执行命令和传输数据的协议及工具。

本地电脑(SSH Client)

        │  TCP 连接,通常到远端 22 端口
        │  里面承载加密、认证后的 SSH 通道

远程服务器(SSH Server / sshd)

它解决三个问题:

  1. 保密性:旁观者看不到命令、密码和传输内容;
  2. 完整性:通信内容被篡改时可以被发现;
  3. 身份认证:客户端确认服务器身份,服务器也确认登录用户身份。

一句话理解:

SSH 是一条经过双向身份检查的加密管道;远程 Shell、文件传输和端口转发,都是这条管道中承载的不同能力。

SSH 不是:

  • Shell 本身:zshbash 才是 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。正确做法是:

  1. 从云厂商控制台、服务器管理员或另一个可信渠道获得 Host Key 指纹;
  2. 对比终端中的 SHA256:...
  3. 确认一致后再接受;
  4. 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.com

macOS 没有 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 -D

macOS 可以把 passphrase 放入 Keychain:

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

这提高的是使用便利性,不代表私钥可以不设 passphrase。

Agent Forwarding 为什么要谨慎

ssh -AForwardAgent 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远端用户名
PortSSH 服务端口
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 yes

Git 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> exit

pwd/ls 操作远端,带 llpwd/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.com

SSH 会在本地建立一个 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 bastion
ssh private-app

ProxyJump 通常比 Agent Forwarding 更安全:连接由本地 SSH 客户端端到端处理,跳板机只转发字节,不需要借用本地 agent 去登录下一跳。


11. 服务器端加固

服务器配置通常位于:

/etc/ssh/sshd_config

一份常见的起点:

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers deploy

但不要直接照抄后重启。安全操作顺序:

  1. 先创建普通管理账号并配置 sudo
  2. 安装并测试公钥登录;
  3. 保持当前 SSH 会话不要关闭;
  4. 修改 sshd_config
  5. 运行配置检查;
  6. reload 服务;
  7. 从另一个新终端测试成功;
  8. 最后才关闭旧会话。
# 检查服务端配置语法
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.com

12.2 常见报错

报错常见原因优先检查
Connection timed out网络不通、防火墙丢包、安全组没开IP、路由、安全组、nc -vz
Connection refused主机可达,但端口无人监听或被主动拒绝sshd 是否运行、端口是否写错
No route to host本机没有到目标网段的可用路由VPN、网关、目标 IP
Permission denied (publickey)用户名/公钥/私钥不匹配或远端权限错误UserIdentityFileauthorized_keys
Too many authentication failuresagent 自动尝试了太多密钥IdentitiesOnly yes + 指定 IdentityFile
Host key verification failedHost Key 未信任或发生变化核验指纹,不要直接关闭检查
REMOTE HOST IDENTIFICATION HAS CHANGED正常换机或中间人攻击先可信核验,再 ssh-keygen -R
Bad permissions私钥或 .ssh 权限过宽chmod 700/600
Broken pipeNAT/防火墙回收空闲连接、网络切换ServerAliveInterval、检查网络

12.3 Permission denied (publickey) 的四个高频原因

  1. 远端用户名错了:云服务器可能使用 ubuntuec2-userdebian 等;
  2. 客户端选错私钥:用 ssh -vvvOffering public key
  3. 公钥没放进目标用户的 authorized_keys
  4. 远端目录或文件权限/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. 延伸阅读与关联概念

官方参考