跳转到内容

第 6 章 · SSH 与远程

一句话:用加密连接登录远程服务器、在上面跑命令、来回传文件,还能把服务器上的端口“拉”到本机来用。 用它还是用别的:登录 Linux 服务器、传部署文件、用 SSH 地址推拉 GitHub 都用它;想在 VS Code 里直接编辑服务器上的代码,用 Remote - SSH 扩展(第 2 章),它底下走的还是这一章的密钥和配置。 核对版本:OpenSSH 9.5(Windows 自带)、OpenSSH 9.6、tmux 3.4、rsync 3.2(Ubuntu 24.04),2026-10

Windows 10(1809 起)和 Windows 11 自带 OpenSSH 客户端,在 PowerShell 里直接能用 ssh。以管理员身份打开 PowerShell 检查,State 是 Installed 就不用装:

终端窗口
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'

显示 NotPresent 时安装客户端:

终端窗口
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

macOS 自带 ssh、scp、sftp,不用装;tmux 要装:

终端窗口
brew install tmux

Ubuntu 和 Debian。只连别人的服务器装第一行;要让这台机器能被别人 SSH 登录,再装 openssh-server:

终端窗口
sudo apt install openssh-client tmux rsync
sudo apt install openssh-server

Windows 本身没有 tmux 和 rsync,在 WSL 里用(第 4 章)。WSL 有自己的 ~/.ssh,和 %USERPROFILE%\.ssh 不是同一个目录:要在 WSL 里再生成一把密钥,或把 config 和私钥拷进去(cp 后 chmod 600),否则别名不认。

查版本:

终端窗口
ssh -V

装了 Git for Windows 的机器上有两个 ssh:系统自带的在 C:\Windows\System32\OpenSSH\,Git Bash 里用的是 Git 自带的那个,版本号可能不同。两者读同一个 %USERPROFILE%\.ssh 目录,密钥和配置通用;WSL 里的 ssh 不读它,见上一段。

  • 密钥对(Key Pair):一对文件。私钥 id_ed25519 留在本机,等于你的密码,不能给任何人;公钥 id_ed25519.pub 可以随便发,放到哪台服务器上,你就能免密码登录哪台。生成时可以给私钥加一个口令(passphrase),私钥文件被人拷走也打不开。
  • authorized_keys:服务器上 ~/.ssh/authorized_keys 文件,一行一个公钥。登录时服务器出一道只属于这次连接的题,本机用私钥签名,服务器拿这个文件里的公钥验证签名,对得上就放行。私钥自始至终没有离开本机,见下图。
  • known_hosts:本机的 ~/.ssh/known_hosts,记录连过的每台服务器的指纹(fingerprint)。第一次连接时 ssh 问你是否信任,答 yes 就记下来;以后同一个地址的指纹变了,ssh 拒绝连接,防止有人冒充服务器。
  • config:本机的 ~/.ssh/config,给主机起别名,把地址、用户名、端口、用哪把私钥写在一起。写好以后 ssh myserver 就够了,scp、rsync、git 和 VS Code 的 Remote - SSH 都认这个别名;WSL 里另有一份,见 6.1。
  • ssh-agent:后台程序,替你保管已经解锁的私钥。私钥有口令时,交给它解锁一次,之后每次连接都不用再输口令。
  • 端口转发(Port Forwarding):借一条 SSH 连接搭隧道。本地转发把服务器上的端口拉到本机,比如服务器上只对本机开放的数据库;远程转发反过来,让服务器访问你电脑上的端口。

SSH 密钥登录:服务器出题,本机用私钥签名,服务器用 authorized_keys 里的公钥验签,私钥不离开本机

<别名> 是 ~/.ssh/config 里 Host 后面的名字,写法见 6.5;没配别名时,所有 <别名> 都可以换成 <用户>@<主机>。表里的 ~/.ssh/… 在 PowerShell 里写成 $HOME\.ssh\…,PowerShell 7.6 之前交给 ssh-add 这类程序的是字面的 ~。tmux 的快捷键在 macOS 上也是 Ctrl,不是 Cmd,先按 Ctrl+B 松开,再按后面的键。

任务 命令 说明
密钥
生成密钥对 ssh-keygen -t ed25519 -C "<邮箱>" 回车存到默认位置,再设口令;-C 只是备注,方便认出是谁的
看公钥,复制这一行 cat ~/.ssh/id_ed25519.pub 一行 ssh-ed25519 AAAA… <邮箱>,要贴到服务器或 GitHub 的就是它
看指纹 ssh-keygen -lf ~/.ssh/id_ed25519.pub 和 GitHub 设置页显示的 SHA256:… 对照,确认是同一把
把公钥装到服务器 ssh-copy-id <用户>@<主机> Linux、macOS、WSL 有;输一次服务器密码
Windows 上装公钥 Get-Content $HOME\.ssh\id_ed25519.pub | ssh <用户>@<主机> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" Windows 没有 ssh-copy-id,这一行做同样的事,服务器是 Linux 时用
把私钥交给 agent ssh-add ~/.ssh/id_ed25519 输一次口令,之后连接不再问;Windows 写 ssh-add $HOME\.ssh\id_ed25519,要先启动服务,见 6.6
看 agent 里有哪些私钥 ssh-add -l 报 Error connecting to agent 就是 agent 没在运行
连接
登录 ssh <用户>@<主机> 第一次会问是否信任指纹,核对后输 yes
指定端口 ssh -p 2222 <用户>@<主机> 服务器的 SSH 不在 22 端口时
用别名登录 ssh <别名> 地址、端口、用户名全在 config 里
只跑一条命令 ssh <别名> "df -h" 输出显示在本地,跑完就断开
排查连不上 ssh -v <别名> 显示读了哪些配置、试了哪把私钥、卡在哪一步
看别名最终展开成什么 ssh -G <别名> 不连接,只列出生效的全部配置,查 config 写没写对
删掉某台主机的旧指纹 ssh-keygen -R <主机> 非 22 端口要写成 ssh-keygen -R "[<主机>]:2222"
传文件
上传文件 scp <本地文件> <别名>:<远程目录>/ 远程路径不以 / 开头时,相对登录用户的家目录
下载文件到当前目录 scp <别名>:<远程文件> . 最后的 . 是本地当前目录
上传整个目录 scp -r <本地目录> <别名>:<远程目录>/ 不加 -r 会报错,见 6.6
增量同步目录 rsync -avz --progress <本地目录>/ <别名>:<远程目录>/ 只传有变化的文件,中断后重跑接着传;源结尾的 / 见 6.6;在 WSL 里用别名先看 6.1
让目标和源完全一致 rsync -avz --delete <本地目录>/ <别名>:<远程目录>/ 慎用:目标里源没有的文件全部删掉;先加 -n 试跑,看清删哪些
交互式传文件 sftp <别名> 进去后用 ls、cd、put <文件>、get <文件>,exit 退出
端口转发
本地转发 ssh -N -L 5433:127.0.0.1:5432 <别名> 本机 5433 接到服务器上的 127.0.0.1:5432;127.0.0.1 是从服务器那边看的地址
远程转发 ssh -N -R 8080:127.0.0.1:3000 <别名> 服务器上访问 127.0.0.1:8080,等于访问你本机的 3000
SOCKS 代理 ssh -N -D 1080 <别名> 浏览器代理设成 SOCKS5 127.0.0.1:1080,流量从服务器出去
只转发、不开 Shell 上面三条里的 -N 终端停在那里不动是正常的,Ctrl+C 关掉隧道
经跳板机登录内网主机 ssh -J <用户>@<跳板机> <用户>@<内网主机> 跳板机和内网主机都要认你的公钥
tmux(Linux、macOS、WSL)
新建会话 tmux new -s <名> 在服务器上开,断线后会话里的程序照跑
离开会话 Ctrl+B 后按 D 会话留在后台继续运行
列出会话 tmux ls 每行一个会话名和窗口数
重新连上 tmux attach -t <名> 回到离开时的画面
左右拆分窗格 Ctrl+B 后按 % 上下拆分按 "
在窗格之间切换 Ctrl+B 后按方向键 当前窗格边框高亮
结束会话 tmux kill-session -t <名> 会话里还在跑的程序一起结束
服务端(Ubuntu)
看 SSH 服务状态 sudo systemctl status ssh Ubuntu、Debian 的服务名是 ssh,CentOS、RHEL 是 sshd
检查配置有没有写错 sudo sshd -t 没有输出就是没错,有错会指出第几行
改配置后生效 sudo systemctl restart ssh 改了 Port 另要两步,见 6.5

服务器管理员给了你 IP、用户名和密码。做完以后用密钥登录,最后关掉密码登录:

  1. 生成密钥对,已经有 ~/.ssh/id_ed25519 就跳过:ssh-keygen -t ed25519 -C "<邮箱>"。
  2. 把公钥装到服务器,输一次服务器密码:ssh-copy-id <用户>@<主机>。Windows 上用 6.3 表里“Windows 上装公钥”那一行。
  3. 测试:ssh <用户>@<主机>,不再问服务器密码就成功了;设了私钥口令的,这里问的是口令。
  4. 在本机的 ~/.ssh/config 里写一个别名,内容见 6.5:code ~/.ssh/config。
  5. 用别名登录,这个窗口保持登录状态,后面出错靠它救回:ssh <别名>。
  6. 在这个窗口里新建一个配置文件,写入 PasswordAuthentication no:sudo nano /etc/ssh/sshd_config.d/00-local.conf。
  7. 检查配置:sudo sshd -t。
  8. 让配置生效:sudo systemctl restart ssh。
  9. 另开一个本机终端,确认密钥还能登录:ssh <别名>。
  10. 确认密码已经被拒绝,看到 Permission denied (publickey). 就对了:ssh -o PubkeyAuthentication=no <别名>。

第 9 步失败时,回到第 5 步那个窗口删掉 00-local.conf,再 sudo systemctl restart ssh。

通过 SSH 隧道访问服务器上的数据库

Section titled “通过 SSH 隧道访问服务器上的数据库”

服务器上的 PostgreSQL 只监听 127.0.0.1:5432,外网连不进去,这是对的。要在本机用图形客户端查数据,借 SSH 搭一条隧道。本地端口用 5433,是因为本机的 5432 可能已经被前言里用 Docker 起的 PostgreSQL 占了:

  1. 确认数据库在服务器上监听:ssh <别名> "ss -ltn | grep 5432",地址是 127.0.0.1:5432 或 0.0.0.0:5432 都行。
  2. 打开隧道,这个终端会停住不动,别关它:ssh -N -L 5433:127.0.0.1:5432 <别名>。
  3. 另开一个终端连本机 5433:psql -h 127.0.0.1 -p 5433 -U <数据库用户>。
  4. 图形客户端(DBeaver、DataGrip 等)里主机填 127.0.0.1,端口填 5433,用户和密码填服务器上数据库的。
  5. 用完回到第 2 步的终端,按 Ctrl+C 关掉隧道。

经常要用,就在 config 的这个主机下加一行 LocalForward 5433 127.0.0.1:5432,以后 ssh -N <别名> 就自带这条隧道。

SSH 本地端口转发:本机客户端连 5433,经 22 端口的加密隧道到服务器 sshd,再由 sshd 连服务器上的 127.0.0.1

在服务器上跑长任务,断线也不怕

Section titled “在服务器上跑长任务,断线也不怕”

直接在 SSH 里跑训练、迁移这类要几个小时的命令,网络一断、合上笔记本,命令就被结束了。放进 tmux 会话里跑:

  1. 登录服务器:ssh <别名>。
  2. 新建一个会话:tmux new -s job。
  3. 在会话里启动任务:python train.py。
  4. 离开会话,任务继续跑:按 Ctrl+B 后按 D。
  5. 断开 SSH,也可以直接关掉终端:exit。
  6. 过几个小时重新登录:ssh <别名>。
  7. 看会话还在不在:tmux ls。
  8. 回到会话,看到的是任务当前的输出:tmux attach -t job。
  9. 任务跑完,结束会话:tmux kill-session -t job。

用 SSH 地址推拉 GitHub 不用每次输令牌(token):

  1. 生成密钥对,已有就跳过:ssh-keygen -t ed25519 -C "<GitHub 邮箱>"。
  2. 把公钥复制到剪贴板。Windows 运行 Get-Content $HOME\.ssh\id_ed25519.pub | Set-Clipboard,macOS 运行 pbcopy < ~/.ssh/id_ed25519.pub,Linux 运行 cat ~/.ssh/id_ed25519.pub 后手动复制那一行。
  3. 在 GitHub 网页右上角头像 → Settings → SSH and GPG keys → New SSH key,粘贴后保存。
  4. 测试,第一次会问是否信任 github.com 的指纹,输 yes:ssh -T [email protected]。看到 Hi <用户名>! You've successfully authenticated, but GitHub does not provide shell access. 就通了。
  5. 克隆,地址在仓库页面 Code 按钮的 SSH 标签下:git clone [email protected]:<用户>/<仓库>.git。
  6. 已经用 HTTPS 地址克隆的仓库,改成 SSH 地址:git remote set-url origin [email protected]:<用户>/<仓库>.git。

配置文件的位置:

范围 路径
客户端配置(Linux、macOS) ~/.ssh/config
客户端配置(Windows) %USERPROFILE%\.ssh\config
已知主机指纹 ~/.ssh/known_hosts,Windows 是 %USERPROFILE%\.ssh\known_hosts
自己的密钥对 ~/.ssh/id_ed25519(私钥)、~/.ssh/id_ed25519.pub(公钥)
服务器上允许登录的公钥 服务器上登录用户的 ~/.ssh/authorized_keys
服务端配置(Linux) /etc/ssh/sshd_config,Ubuntu 还读 /etc/ssh/sshd_config.d/*.conf
服务端配置(Windows) %ProgramData%\ssh\sshd_config

客户端值得写的项:

配置项 建议值 效果
Host 一个好记的别名,如 myserver 之后 ssh myserver 就能连
HostName 服务器 IP 或域名 别名实际指向的地址
User 登录用户名 不用每次写 <用户>@
Port 服务器的 SSH 端口 默认 22,不是 22 时才写
IdentityFile ~/.ssh/id_ed25519 有多把私钥时指定用哪把
ServerAliveInterval 60 每 60 秒发一次心跳,空闲时连接不被防火墙掐断
ProxyJump 跳板机的别名 等于每次加 -J,内网主机也能直接 ssh <别名>
AddKeysToAgent yes 第一次用私钥时自动交给 ssh-agent,之后不再问口令

同一项写了多次时,ssh 取第一次读到的值,所以具体主机写在前面,Host * 的通用设置放在文件最后。最小配置,写进 ~/.ssh/config:

# 别名,之后 ssh myserver 就能连
Host myserver
# 服务器地址
HostName 203.0.113.10
# 登录用户名
User deploy
# SSH 端口,是 22 时可以不写
Port 2222
# 用哪把私钥
IdentityFile ~/.ssh/id_ed25519
# 经过下面的跳板机连过去,不需要就删掉这行
ProxyJump bastion
# 跳板机
Host bastion
HostName 203.0.113.5
User ops
# 对所有主机生效的设置放最后
Host *
# 每 60 秒发一次心跳,防止空闲断线
ServerAliveInterval 60
# 私钥第一次解锁后交给 ssh-agent
AddKeysToAgent yes

服务端值得改的三项。改之前务必保持一个已登录的会话不关,改完另开一个终端测试,确认能登录再关掉旧会话:

配置项 建议值 效果
PasswordAuthentication no 只能用密钥登录,挡住全网扫描的密码爆破
PermitRootLogin prohibit-password root 只能用密钥登录;sshd -T 里显示成 without-password,是同一个意思
Port 如 2222 日志里的扫描少很多;防火墙和云安全组要先放行新端口

服务端的配置也是第一次读到的值生效。Ubuntu 的 sshd_config 开头就读入 sshd_config.d/ 下的文件,云服务器镜像常在那里放一个写着 PasswordAuthentication yes 的 50-cloudimg-settings.conf,直接改 sshd_config 不起作用。所以自己的设置写进 /etc/ssh/sshd_config.d/00-local.conf,文件名以 00 开头排在最前:

# 只允许密钥登录
PasswordAuthentication no
# root 只能用密钥登录
PermitRootLogin prohibit-password

写完用 sudo sshd -T | grep -i passwordauthentication 看最终生效的值。Ubuntu 22.10 起 SSH 默认由 ssh.socket 监听端口,改了 Port 要运行 sudo systemctl daemon-reload 和 sudo systemctl restart ssh.socket,只重启 ssh 端口不会变。

  • <用户>@<主机>: Permission denied (publickey). 原因:服务器上没有你的公钥,或 ~/.ssh 权限太宽被 sshd 忽略。处理:在服务器上 chmod 700 ~/.ssh 和 chmod 600 ~/.ssh/authorized_keys;ssh -v 看试了哪把私钥。
  • WARNING: UNPROTECTED PRIVATE KEY FILE!,私钥被忽略。 原因:私钥文件别人也能读。处理:chmod 600 ~/.ssh/id_ed25519;Windows 上 icacls $HOME\.ssh\id_ed25519 /inheritance:r /grant:r "$($env:USERNAME):(R)"。
  • WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!,连接被拒绝。 原因:服务器重装过、换了 IP 上的机器,或真有人冒充。处理:向管理员核实指纹后,ssh-keygen -R <主机> 删掉旧记录再连。
  • 闲置几分钟后卡死,最后报 client_loop: send disconnect: Broken pipe。 原因:中间的路由或防火墙掐掉了没有流量的连接。处理:在 ~/.ssh/config 的 Host * 下写 ServerAliveInterval 60。
  • 改完 sshd 配置重启后,ssh: connect to host <主机> port 22: Connection refused 或一直超时。 原因:配置写错或新端口没放行,把自己锁在了外面。处理:改前保持一个已登录的会话,先 sudo sshd -t;已锁死只能用云控制台的网页终端登录修复。
  • scp 拷目录报 scp: local "<目录>" is not a regular file,什么都没拷过去。 原因:没加 -r,scp 默认只拷文件。处理:scp -r <本地目录> <别名>:<远程目录>/;下载目录同样要加 -r。
  • rsync 同步后远程多套了一层目录,成了 <远程目录>/src/。 原因:源写成 src 是把目录本身拷过去,写成 src/ 才是只拷里面的内容。处理:想让两边内容一致,源一律以 / 结尾,如 rsync -avz src/ <别名>:<远程目录>/。
  • Windows 上 ssh-add 报 Error connecting to agent: No such file or directory。 原因:该服务默认禁用。处理:以管理员身份运行 Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent。
  • Windows 上 agent 已加好私钥,git push 仍然每次问口令。 原因:Git 用的是自带的 ssh,不找 Windows 的 ssh-agent。处理:git config --global core.sshCommand C:/Windows/System32/OpenSSH/ssh.exe。
  • OpenSSH 手册:ssh、scp、sftp、ssh-keygen、sshd 等每个程序的官方手册页入口。
  • ssh_config 手册:~/.ssh/config 里每个配置项的准确含义和默认值。
  • Windows 上的 OpenSSH:在 Windows 上安装 OpenSSH 客户端和服务器、启动服务、放行防火墙。
  • GitHub:使用 SSH 连接:生成密钥、添加到 GitHub 账号、测试连接和排错,有中文。
  • tmux wiki:tmux 官方入门、快捷键和配置说明。