ssh-config —— ~/.ssh/config 客户端配置文件
一句话定义
~/.ssh/config 是 OpenSSH 客户端(ssh、scp、sftp、rsync -e ssh、git over ssh 都共用它)的"连接档案"——把"用什么用户、走哪个端口、用哪把私钥、是否走跳板机、是否端口转发"统统压缩成一个别名。
典型场景
- 训练营 5 节点 + 阿里云 10 节点 + 跳板机 + GitHub 多账号 = 15+ 条不同的连接配置。命令行里每次手写
ssh -i ~/.ssh/id_rsa -p 26292 root@103.47.83.39是不可能的。 Day0-setup.md§2.4:给 5 节点集群写好 alias 之后,ssh m1 'kubectl get nodes'直接出结果。
它和 /etc/hosts 完全不是一回事
这是新手最常误解的点。
| 维度 | /etc/hosts | ~/.ssh/config |
|---|---|---|
| 管谁 | 整个操作系统的名字解析(ping、curl、ssh 都生效) | 只管 OpenSSH 工具链 |
| 能做什么 | 只能做一件事:主机名 → IP | 用户、端口、私钥、跳板、端口转发、保活…几十项 |
| 作用域 | 系统级(要 sudo 改) | 用户级(自己改自己的,互不干扰) |
| 写完之后 | ping m1 会通 | ping m1 不通,但 ssh m1 通 |
简单说:/etc/hosts 只解决"名字",ssh config 解决"怎么连"。 两者可以同时用 —— /etc/hosts 写 10.0.24.28 m1,让所有工具都能用 m1 这个名字;~/.ssh/config 再额外补上 User、IdentityFile 等。
配置文件位置和优先级
OpenSSH 找配置的顺序,前者优先:
- 命令行参数:
ssh -p 2222 -i ~/.ssh/foo user@host—— 最高优先级 ~/.ssh/config—— 当前用户的私人配置/etc/ssh/ssh_config—— 系统级,影响所有用户- OpenSSH 内置默认值
权限要求严格:chmod 600 ~/.ssh/config(其他用户能读到你的配置 = 泄露你的内网拓扑,所以 ssh 会拒绝跑)。
基本语法:Host 段
Host <alias> [alias2 alias3 ...]
Key1 Value1
Key2 Value2
...
Host <another-alias>
...
规则:
Host行下面缩进的字段,只对这个段生效(缩进只是习惯,OpenSSH 不强制,但强烈建议)- 字段不区分大小写:
HostName/hostname/HOSTNAME等价 - 一行
Host可以写多个别名,空格分隔(详见下一节) - 段之间用空行分隔(不强制,但便于阅读)
多别名写法(训练营常用)
Host k8s-cp-1 m1
HostName 10.0.24.28
User root
IdentityFile ~/.ssh/id_rsa
Host k8s-cp-1 m1 表示 k8s-cp-1 和 m1 都是这个段的别名,下面的配置对两者都生效。所以 ssh k8s-cp-1 和 ssh m1 完全等价。
为什么这么做:
- 正式名 + 短名共存:脚本里写
k8s-cp-1自描述,手敲时用m1省力 - 不需要写两遍配置:传统做法是
Host k8s-cp-1和Host m1各写一段,重复且容易漂移
最常用字段表
完整列表见
man ssh_config,下面是 90% 场景会用到的。
| 字段 | 作用 | 示例 |
|---|---|---|
HostName | 真实的 IP 或域名(重点:可以和 Host 别名不同) | HostName 154.201.73.31 |
User | 登录用户 | User root |
Port | SSH 端口(默认 22) | Port 26292 |
IdentityFile | 用哪把私钥 | IdentityFile ~/.ssh/id_ed25519_work |
IdentitiesOnly | 只用 IdentityFile 指定的 key,忽略 ssh-agent 里的其它 | IdentitiesOnly yes |
ProxyJump | 经过哪台跳板机(OpenSSH 7.3+ 推荐写法) | ProxyJump bastion |
ProxyCommand | 自定义连接命令(走 SOCKS5/HTTP 代理) | ProxyCommand nc -X 5 -x 127.0.0.1:7890 %h %p |
LocalForward | 本地端口转发(外面访问 localhost:8080 = 远端的 80) | LocalForward 8080 localhost:80 |
RemoteForward | 远程端口转发(远端访问 localhost:9000 = 本地服务) | RemoteForward 9000 localhost:3000 |
DynamicForward | 把 ssh 当 SOCKS5 代理 | DynamicForward 1080 |
ServerAliveInterval | 多少秒没数据就发一次保活包(防 NAT 断流) | ServerAliveInterval 30 |
ServerAliveCountMax | 连续多少次保活失败才放弃 | ServerAliveCountMax 3 |
StrictHostKeyChecking | 首次连接的指纹校验策略 | accept-new / no / yes |
UserKnownHostsFile | 自定义 known_hosts 位置 | /dev/null(一次性机器,不留指纹) |
PreferredAuthentications | 认证方式优先级 | publickey,password,keyboard-interactive |
ForwardAgent | 把本地 ssh-agent 转发到远端(远端用本地 key) | ForwardAgent yes(小心安全) |
StrictHostKeyChecking 三个取值
| 值 | 行为 | 适用 |
|---|---|---|
yes | 严格:远端 key 必须在 known_hosts 里,否则拒连 | 生产 / 敏感环境 |
accept-new | 首次自动信任并写 known_hosts(TOFU),之后严格 | 推荐默认 —— 平衡安全和体验 |
no | 不校验也不写 known_hosts | 一次性的临时机器(云厂商动态实例) |
阿里云那种"今天创建、明天销毁"的临时实例,写 no + UserKnownHostsFile /dev/null 可以避免每次 IP 变了都要手动清 known_hosts。
通配符与字段继承
匹配规则:自上而下扫描,所有命中的段合并;同一个字段,第一次出现的值生效(不是最后一次!)。
通配符 * ?
Host *.prod.example.com
User deploy
IdentityFile ~/.ssh/id_prod
Host web-? db-?
User admin
ProxyJump bastion
*匹配任意字符(含空)?匹配单个字符web-?能匹配web-1、web-a,匹配不了web-12
继承("全局兜底"模式)
Host github.com
HostName github.com
User git
ProxyCommand nc -X 5 -x 127.0.0.1:7890 %h %p
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
Host * 段放在文件末尾或开头都行(OpenSSH 是合并而不是覆盖),它给所有连接补充保活配置。GitHub 段已经有 User,Host * 不会覆盖它("第一次出现的值生效"原则)。
⚠️ 反直觉的地方:先写的优先。如果你想让某台机器的
User被全局规则覆盖,办法是去掉这台机器的User字段、让它"继承"全局。
Match 块(更精细的条件)
Match host *.internal user deploy
IdentityFile ~/.ssh/id_deploy
Match 比 Host 强大得多 —— 可以按 user、host、exec(执行外部命令)等多条件匹配。日常用得少,记得它存在即可。
实战示例
场景 1:K8s 训练营 5 节点
来自 Day0-setup.md:
Host k8s-cp-1 m1
HostName 10.0.24.28
User root
IdentityFile ~/.ssh/id_rsa
Host k8s-cp-2 m2
HostName 10.0.24.29
User root
IdentityFile ~/.ssh/id_rsa
# ... 后续 3 个节点同理
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
用法:
ssh m1 # 登 cp-1
ssh m4 'kubectl get nodes -o wide'
scp -r ./manifests m1:/root/ # scp 也会读 ssh config
rsync -av ./data m2:/data/ # rsync 默认走 ssh,也认 config
场景 2:跳板机(公司常见)
公司机器只能从跳板机进,传统做法是先 ssh bastion,再 ssh dev-01,烦。用 ProxyJump 一步到位:
Host bastion
HostName 124.236.26.209
Port 2224
User huoyuanjun
Host dev-01
HostName 10.1.0.34
User root
ProxyJump bastion # 关键
ssh dev-01 —— OpenSSH 自动先连 bastion,再从 bastion 内开一条到 10.1.0.34 的隧道。中间所有流量都被 ssh 加密包装。
也可以多级跳:ProxyJump bastion1,bastion2。
场景 3:GitHub 多账号
一台机器,要在两个 GitHub 账号(个人 + 公司)下提交。两套 key、两套 user.email。
# 个人账号
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
# 公司账号(注意 Host 是自定义别名)
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
clone 时:
git clone git@github.com:me/personal-repo.git # 走个人 key
git clone git@github-work:company/work-repo.git # 走公司 key(注意 host 是 github-work)
IdentitiesOnly yes 必须加。否则 ssh 会先把 ssh-agent 里所有 key 都试一遍,GitHub 看到第一把不对的 key 直接拒连(API rate-limit 还会被 ban)。
场景 4:通过 SOCKS5 代理访问 GitHub
国内访问 GitHub 经常超时。如果你已经有本地 Clash/V2Ray 在 7890 端口提供 SOCKS5 代理:
Host github.com
HostName github.com
User git
ProxyCommand nc -X 5 -x 127.0.0.1:7890 %h %p
nc -X 5 -x 127.0.0.1:7890 %h %p 让 nc 通过 SOCKS5(-X 5)连到 %h:%p(OpenSSH 把 hostname 和 port 替换进去)。SSH 把 nc 的 stdin/stdout 当成隧道用。
端口转发(一句话理解)
| 方向 | 字段 | 谁能访问 | 用途 |
|---|---|---|---|
| 本地 → 远程 | LocalForward L:R | 本机访问 localhost:L → 远端的 R | 把远端服务"拉到"本地用 |
| 远程 → 本地 | RemoteForward R:L | 远端访问 localhost:R → 本机的 L | 把本机服务"推给"远端用 |
| 动态(SOCKS) | DynamicForward P | 本机的 SOCKS5 代理在 P 端口 | 把 SSH 当翻墙工具 |
典型例子:从本地访问远端 K8s dashboard:
Host k8s-cp-1 m1
HostName 10.0.24.28
User root
LocalForward 8001 localhost:8001
ssh m1 后挂着不退出,本地浏览器开 http://localhost:8001 就是远端 kubectl proxy 的页面。
也可以临时用命令行:ssh -L 8001:localhost:8001 m1,效果一样、但不持久。
调试:ssh -G 和 ssh -v
ssh -G <alias> —— 看最终生效配置
不实际连接,把所有 Host 段合并后的最终值打印出来。改 config 之后必跑这个,比 ssh <alias> 试错快得多 —— 连不上你不知道是网络问题还是配置写错。
ssh -G m1 | grep -E "^(hostname|user|identityfile|port)"
# hostname 10.0.24.28
# user root
# port 22
# identityfile ~/.ssh/id_rsa
ssh -v / -vv / -vvv —— 看握手过程
ssh -v m1 'echo OK'
-v 显示连接日志(哪把 key 被尝试、远端响应什么)。90% 的"为什么连不上"问题用 -v 看完就懂了。
最常见的两类信号:
Offering public key: /home/.../id_rsa然后server refused our key→ key 没在远端 authorized_keysAuthentications that can continue: password→ 远端禁了 pubkey 认证(见sshd.md)
常见踩坑
坑 1:明明写了 IdentityFile,还是用了别的 key
原因:ssh-agent 里有其它 key。OpenSSH 的默认行为是先尝试 agent 里所有 key,再用 IdentityFile。
修复:
IdentitiesOnly yes
加上这个,强制只用 IdentityFile 指定的那把。
坑 2:Permissions are too open 拒连
Bad permissions for ~/.ssh/config
修复:
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/id_* # 私钥同样要 600
chmod 700 ~/.ssh # 目录 700
坑 3:Host * 写在最前面,全局配置覆盖不掉特定段
不会被覆盖(OpenSSH 是"先到先得"),但很多人以为会,结果配置混乱。
正确做法:
- 把特定段(具体的 Host)写在前面
- 把通配段(
Host *、Host *.example.com)写在后面 - 这样阅读顺序和优先级一致,不绕。
坑 4:改了 config 但 git 还是用错的 key
git 是 fork 出来跑 ssh 的,所以 git 默认会读 ~/.ssh/config。但有几种例外:
- 你用的是 GUI 客户端(SourceTree / Tower),可能有自己的 ssh 集成
- 你在
~/.gitconfig里设了core.sshCommand = /usr/bin/ssh -i ~/.ssh/some_key(会覆盖 config)
排查:GIT_SSH_COMMAND='ssh -v' git fetch 看实际跑的命令。
坑 5:alias 和 HostName 同名导致 DNS 解析意外发生
Host github.com # alias = github.com
HostName github.com # 这里又写一遍真实 hostname
ProxyCommand ...
这种重名是允许的,OpenSSH 不会混淆 —— 它先用 alias 匹配段,然后用 HostName 解析。但读起来容易绕,建议给 alias 起个能区分的名字(如 github-personal),除非你确实想让 ssh github.com 直接用这段。
关联命令
- ssh —— 客户端命令本身
- ssh-keygen —— 生成
IdentityFile指向的密钥 - sshd —— 远端守护进程(很多"连不上"的根因在远端)
man ssh_config—— 完整字段参考