共计 6966 个字符,预计需要花费 18 分钟才能阅读完成。
和服务器打交道久了,总会积累出一堆记不住的东西:这台机器的 SSH 端口不是 22,那台要换一个用户名,还有一台得先跳过一层堡垒机才进得去。早些年我是靠翻终端历史记录过日子的,ctrl-r 搜一串 IP,搜不到就去翻笔记,换台电脑历史记录一没就彻底抓瞎。后来老老实实把这些细节都写进 ~/.ssh/config。
先从最基础的说起。要远程连接 VPS 或者类 Linux 服务器,都可以用 SSH 客户端通过密码或者公私钥的方式连接,而 SSH 客户端会在用户目录下的 .ssh 目录里存放相关的配置信息,其中最常打交道的就是 .ssh/config 这个配置文件。
SSH Config 能用在非常多的地方。比如配置 [[GitHub]] / [[GitLab]] 通过 SSH 协议访问,给一台记不住 IP 的服务器起个昵称然后免密码登录;这篇文章后面还会介绍到的 [[Herdr]] 远程控制多台机器,同样离不开 SSH 配置,我之前写过的 Herdr 使用技巧,把一群 Coding Agent 用出花来 里,多机器管理那部分就完全建立在 SSH Config 之上。把这套东西弄明白之后,后续再用 [[assh]] 这类管理工具也会轻松很多,因为它们本质上都是在帮你生成这份配置。
除了省掉重复输入,SSH Config 还能解决一些体感很明显的问题。比如用 ControlPath、ControlPersist 这些参数实现连接复用,第一次连上之后的一段时间内,即使断开也能以极快的速度重连,省掉每次重新握手和认证的等待,这部分后面会专门展开讲。
基本写法
每台主机的配置以 Host 开头作为别名,以下是常用的字段:
Host myserver
HostName example.com
User yourname
Port 22
IdentityFile ~/.ssh/id_ed25519
说明
- Host 是自己起的别名,实际输入的时候使用 ssh myserver
- HostName 真实的主机名或 IP 地址
- User 登录用户名
- Port SSH 端口,默认 22
- IdentityFile,指定用于认证的私钥文件路径
如果同时管理多台服务器,可以在同一个文件里为每个服务器写一个 Host 块,各自独立配置,相互不影响。
进阶配置
除了一些基础字段,SSH Config 还支持一些进阶的用法。比如跳板机 ProxyJump host,不用自己手动 ssh ,还可以配置通配符,通过 Host *.example.com 对一批主机统一设置参数等等。
ProxyJump 跳板机配置
一些企业为了安全和统一管理,会在内网和外网之间部署堡垒机(Bastion Host,也叫 Jump Server、跳板机),集中管理运维人员对服务器、网络设备、数据库等资产的访问。所有的运维操作都必须先登录堡垒机,再由堡垒机代理跳转到目标资产,不能直接连接。堡垒机通常有完善的身份认证、权限管控、操作审计和风险拦截能力。
ProxyJump 简写 -J 通常用于一台跳板机(堡垒机)连接到内网中无法直接访问的目标服务器,SSH 会自动完成中转,不需要先手动登录跳板机然后再使用 SSH 连接。
# 先定义跳板机
Host my-jump-box
HostName 192.168.1.50 # 跳板机 HostName
Port 22002
User jump_admin
IdentityFile ~/.ssh/id_rsa_jump
# 目标服务器,引用跳板机别名
Host my-target-server
HostName 10.0.1.100
Port 22001
User dev_user
IdentityFile ~/.ssh/id_rsa_target
ProxyJump my-jump-box
配置好后直接 ssh my-target-server 即可自动经跳板机登录,scp 或者 [[VS Code]] 的 Remote-SSH 也都可以使用。ProxyJump 支持多级跳板,直接使用逗号分割即可,ProxyJump bastion,middleware SSH 会按照次序跳转。如果只是临时用一次,也可以直接写成这种形式 ssh -J user1@host1:port1,user2@host2:port2 user3@host3。
通配符 Host 匹配规则
Host 支持 * 和 ? 通配符,可以对一批主机批量设置公共参数,减少重复配置。使用时有一个关键的原则:SSH 按文件从上到下顺序匹配。遇到多个匹配块时,先出现的值会优先生效,后面的同名参数不会覆盖前面的。
# 具体主机放前面
Host github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
# 通配符放最后,作为兜底默认值
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
IdentitiesOnly yes
这一点和大多数配置文件的习惯正好相反,SSH 是首次匹配生效,而不是后者覆盖前者。所以一旦把 Host * 写在文件开头,后面所有针对具体主机的专属配置都会失效,排查起来会非常困惑。记住这个顺序:具体主机的专属配置放在前面,星号通配符作为兜底默认值放在文件最后。
ForwardAgent 密钥转发
ForwardAgent 让本地 ssh-agent 中加载的私钥可以被转发到远程服务器上使用,就像密钥本身就在远程主机一样,常见的使用场景是:登录服务器后,还需要使用本地的密钥去拉取 GitHub 仓库,或者跳转到下一台主机。
Host server1
HostName 106.187.36.100
User einverne
ForwardAgent yes
配置好后,无需每次添加 -A 参数,直接使用 ssh server1 即可生效。注意这需要客户端设置 ForwardAgent yes,同时远程服务器的 sshd_config 中也需要开启 AllowAgentForwarding yes 才可以使用。不过出于安全考虑,只对信任的服务器开启,避免在不可信的跳板机上使用。
LocalForward 本地端口转发
LocalForward 用于把远程服务器的端口映射到本地,常用于访问只能在内网访问的数据库,应用等。
Host tunnel
HostName db.internal
User dba
LocalForward 15432 localhost:5432
配置后执行 ssh tunnel 访问本地的 localhost:15432 就相当于访问远程服务器中的 localhost:5432 。一个 Host 块里可以写多条 LocalForward 同时转发多个端口,比如同时映射数据库和 [[Redis]]。
Host multiservice
HostName app.internal
User admin
LocalForward 5432 db.internal:5432
LocalForward 6379 redis.internal:6379
当然如果我们不使用上面的配置,也可以使用 ssh -L 来将远程服务器中的端口映射到本地端口。
ssh -L [绑定地址:]本地端口:目标地址:目标端口 username@server-ip
- 绑定地址:可选,省略时默认只绑定 127.0.0.1,也就是只有本机能访问。
- 本地端口:在你的电脑上监听的端口,可以自己随便选一个未被占用的。
- 目标地址:目标端口:SSH 服务器登录后要访问的目标服务地址和端口,这个目标地址不一定是 SSH 服务器本身,可以是它能访问到的其他任意主机。
比如远程服务器 remote_host 上有一个运行的 [[MySQL]] 服务端口是 3306,但是没有开启远程访问,只允许服务器本地访问,那如果我们要访问该 MySQL 端口,可以使用如下的命令
ssh -L 3000:localhost:3306 user@remote_host
运行该命令之后我们就可以在我们本地电脑上通过本地的 localhost:3000 来访问到 MySQL。
RemoteForward 远程端口转发
RemoteForward 和 LocalForward 方向相反,它把本地的服务暴露到远程服务器上,常见的场景是本地跑着一个开发中的服务,想让内网里的同事或者远端的回调能访问到。
Host demo
HostName demo.example.com
User dev
RemoteForward 8080 localhost:3000
配置之后执行 ssh demo,远端机器上访问 localhost:8080 就相当于访问你本机的 3000 端口。需要注意的是,默认情况下远端只会绑定 127.0.0.1,也就是只有登录到那台服务器上才能访问。如果希望内网其他机器也能访问,需要远端的 sshd_config 里开启 GatewayPorts yes,这个开关有明显的安全含义,不要在公网机器上随手打开。
DynamicForward 动态端口转发
前面两个转发都要事先指定目标地址和端口,如果目标不固定,可以用 DynamicForward 在本地起一个 SOCKS5 代理,由远端服务器代为访问。
Host socks
HostName vps.example.com
User einverne
DynamicForward 1080
执行 ssh -N socks 之后,本地的 127.0.0.1:1080 就是一个 SOCKS5 代理,浏览器或者其他支持 SOCKS 的程序指过去,所有流量都会从那台服务器出去。-N 表示只建立隧道不执行远程命令,配合 -f 可以让它转到后台运行。这其实就是 ssh -D 1080 user@host 的配置文件写法。
ControlMaster 连接复用
这是开头提到过的一组参数,也是我认为收益最高的一项。默认情况下每次 ssh 都要重新完成一整套 TCP 握手、密钥协商和身份认证,如果你的跳板链路比较长,或者网络延迟比较高,每次连接都要等上两三秒。连接复用的思路是让第一条连接充当 master,后续连到同一台主机的会话直接复用这条已经建立好的通道。
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%C
ControlPersist 10m
三个参数各司其职:ControlMaster auto 表示没有 master 就新建一条,有就复用;ControlPath 指定 master 连接的 Unix socket 存放路径;ControlPersist 10m 表示即使你退出了所有会话,master 连接仍然在后台保留 10 分钟,这段时间内重连几乎是瞬间完成的,写成 yes 则是一直保留到手动关闭。
ControlPath 里的 %C 是一个哈希占位符,展开后是本地主机、远程主机、端口、用户名和跳板信息的哈希值。早期常见的写法是 %r@%h:%p,但 Unix socket 的路径长度有上限,主机名一长就会报错,用 %C 可以规避这个问题。另外记得先手动建好目录,SSH 不会自动创建:
mkdir -p ~/.ssh/sockets
master 连接是可以手动管理的,ssh -O check demo 查看某台主机的 master 是否还活着,ssh -O exit demo 主动关掉它。有一点要提前知道:复用意味着所有会话共享同一条 TCP 连接,master 断了所有子会话会一起断,另外在跑大文件传输的时候可能会互相抢带宽,介意的话可以对特定主机单独关掉 ControlMaster no。
Include 拆分配置文件
当配置膨胀到几百行之后,单文件就很难维护了,Include 可以把配置拆成多个文件。
Include ~/.ssh/config.d/*.conf
Host *
ServerAliveInterval 60
相对路径会相对于 ~/.ssh 解析,所以上面也可以简写成 Include config.d/*.conf。我的习惯是按用途拆:work.conf 放公司的机器,home.conf 放家里的设备,vps.conf 放各家云服务商的机器,改动的时候互不干扰,也方便单独同步或者加密其中某一份。
要注意 Include 是在解析到那一行的时候就地展开的,而 SSH 又是首次匹配生效,所以 Include 应该放在文件最前面,兜底的 Host * 依旧放最后。
AddKeysToAgent 与 UseKeychain
如果你的私钥设置了密码,每次连接都要输一遍是很烦的。AddKeysToAgent yes 可以让 SSH 在第一次用到某个私钥的时候自动把它加进 ssh-agent,之后同一个 agent 生命周期内就不用再输了,省掉了手动 ssh-add 这一步。
macOS 上还有一个 Apple 自己加的选项 UseKeychain yes,它会把私钥密码存进系统钥匙串,重启之后也不用重新输入。
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
UseKeychain 是 macOS 专属的,写在 Linux 上的配置里会直接报错说不认识这个关键字。如果你的 dotfiles 是跨平台共用的,可以用 Match 条件把它隔离开,或者干脆放进只在 macOS 上 Include 的那个文件里。
用 ssh -G 排查最终生效的配置
通配符、Include、首次匹配生效这几件事叠在一起,很容易出现「我明明配了却没生效」的情况。这时候不要靠肉眼从上往下读配置,直接用 ssh -G 让 SSH 自己告诉你结论:
ssh -G my-target-server
它会打印出针对这个目标最终生效的全部参数,包括你没显式配置的默认值,而且不会真的发起连接。想确认某一项的时候配合 grep 更方便:
ssh -G my-target-server | grep -iE "hostname|user|port|identityfile|proxyjump"
我排查配置问题基本都是从这条命令开始的,比反复试连接快得多。如果还想看连接过程本身,再加 ssh -vvv 看详细握手日志。
配置文件权限
最后一个容易踩的坑是权限。SSH 对 ~/.ssh 目录和里面文件的权限检查很严格,如果 ~/.ssh/config 或者私钥文件对同组用户、其他用户可写,SSH 会直接拒绝使用,报 Bad owner or permissions 之类的错误。
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
用 Git 管理 dotfiles 的时候这个问题特别常见,因为 Git 只记录可执行位,不保留完整权限,在新机器上 clone 下来之后往往需要手动 chmod 一遍。
assh
当手上的机器多到一定程度,手写 ~/.ssh/config 就开始力不从心了。大量重复的 ServerAliveInterval、ControlPath 要一遍遍复制粘贴,想改一个默认值得改几十处,配置文件很快就膨胀到上千行。assh 是为了解决这个问题而生的上层管理工具,它用 YAML 描述所有主机,支持全局 defaults、模板继承和正则匹配 Host,最后编译成一份标准的 ~/.ssh/config。对 ssh、scp、rsync 这些客户端来说整个过程是完全透明的,它们读到的仍然是原生格式的配置文件。
macOS 上用 Homebrew 安装:
brew install assh
默认的配置文件位置是 ~/.ssh/assh.yml,内容大致是这样:
hosts:
ser8ubuntu2404:
Comment:
- "Home server Ubuntu 24.04 VM"
HostName: 192.168.2.101
PreferredAuthentications: publickey
IdentityFile: ~/.ssh/id_rsa
User: einverne
Port: 22
defaults:
ForwardAgent: yes
ServerAliveInterval: 20
ServerAliveCountMax: 30
TCPKeepAlive: no
ControlMaster: auto
# ControlPath: ~/.ssh/connection-%r-%h-%p.sock
ControlPath: /tmp/connection-%h-%p.sock
ControlPersist: yes
Compression: yes
这份配置里 defaults 段落是 assh 最省事的地方,ForwardAgent、连接保持、连接复用这些参数写一次,所有主机自动继承,不用在每个 Host 块里重复。写完之后执行下面的命令编译:
assh config build > ~/.ssh/config
通过 assh 编译之后就会自动生成如下的配置

最后
SSH Config 的核心价值,是把连接一台机器所需要的全部细节一次性固化下来。主机名、端口、用户名、私钥、跳板路径、端口转发,这些东西本来每次连接都要重新想一遍,写进配置之后就只剩一个别名。别名越短,心智负担越低,这也是为什么我现在连内网机器都只敲 ssh ser8。
再往上一层,很多工具的远程能力其实都是直接复用 SSH Config 的,VS Code 的 Remote-SSH、rsync、Ansible,以及前面提到的 Herdr 多机器管理,都是先读你的 ~/.ssh/config 再决定怎么连。所以把这个文件整理干净,收益会一路传导到整条工具链上。如果配置多到自己都记不住,再上 assh 做一层管理,也不迟。
