动机和初心
最近用轻量的Ghostty作为ssh终端。抛弃了其他重型的。那么跳板机就成为了个问题。索性彻底折腾了下,不用再搞那个config配置。
assh也支持include分拆【语法不同】不过目前几十个小鸡用一个文件这样管理也可以了,简练了很多毕竟。
assh (Advanced SSH Config)
是一个基于 Go 编写的增强型 SSH 工具。它的核心逻辑是:用更现代、更强大的方式编写配置,然后将其动态生成或“翻译”给原生的 SSH 客户端。
https://github.com/moul/assh
对于管理几十个甚至上百个 VPS 节点的开发者来说,它是从“手动配置”进化到“配置即代码”的神器。
🚀 assh 的核心功能
- 配置继承 (Inheritance): 像面向对象编程一样,定义一个
base模板,其他 Host 直接继承。 - 正则表达式支持: 可以用
Host /^prod-(.*)/这种语法批量匹配机器。 - 透明网关 (Gateways): 极大简化多级跳板机配置,支持链式跳转(A -> B -> C)。
- 动态主机: 可以通过钩子 (Hooks) 或脚本动态决定连接参数。
- 别名与重写: 支持比原生更灵活的别名系统。
- 本地/远程命令执行: 在连接前、连接后自动执行特定脚本。
实现的功能
根据区域跳板,命名如eu,us开头,直接ssh目标机就实现了优化机代理的目的。
assh可以和原来的.ssh/config配合使用,但每次修改都要build一下,我觉得比较麻烦。从实用的角度,我选择让assh全面接管。因为我通常只需要跳板机的功能。
#要在config里用Include config.assh
#本文舍弃这个方案
assh config build > ~/.ssh/config.assh1. 安装
对于大部分系统,直接下载二进制文件或使用包管理器:
- macOS:
brew install assh Linux:
curl -L https://github.com/moul/assh/releases/latest/download/assh-linux-amd64 > /usr/local/bin/assh && chmod +x /usr/local/bin/assh2. 使用SSH 的‘透明代理’
ProxyCommand功能。备份你的.ssh/config后,新建一个config,里面只包含:
# 只有这一段,让 assh 接管一切 Host * ProxyCommand assh connect --port=%p %h
3.示例 我的配置assh.yml
⚠️注意:YAML 对空格极其敏感。如果你的子项缩进没有对齐,或者混用了空格和 Tab,就会报错。
因为不编译,所以需要这个‘缓存’文件夹。
新建下先:
mkdir ~/.ssh/sockets/nano ~/.ssh/assh.yml
defaults:
# 这里的参数会被编译成 Host *
ServerAliveInterval: 60
ControlMaster: auto
ControlPath: ~/.ssh/sockets/%r@%h:%p
# --- 连接稳定性 ---
ServerAliveInterval: 60
ServerAliveCountMax: 5
TCPKeepAlive: yes
# --- 性能优化 (Multiplexing) ---
# 开启连接复用:第一个连接建立后,后续连接秒开,无需重复握手
ControlMaster: auto
ControlPersist: 4h
Compression: yes
# --- 安全与身份验证 ---
# 只使用指定的密钥,防止 SSH 尝试 Agent 中所有的密钥导致触发服务器防火墙
IdentitiesOnly: yes
# 连接时自动将密钥添加到 ssh-agent,省去手动 ssh-add
AddKeysToAgent: yes
# --- 体验优化 ---
# 禁用 GSSAPI 认证,能显著加快连接内网服务器的速度
GSSAPIAuthentication: no
# 第一次连接新服务器时,自动询问并记录,而不是直接报错
##############以下可选开启############
StrictHostKeyChecking: ask
# 以图形化形式显示主机指纹(方便肉眼比对安全)
VisualHostKey: yes
# 1. 自动接受并添加新的 host key (不再询问 yes/no)
##StrictHostKeyChecking: "no"
StrictHostKeyChecking accept-new
# 2. 将 known_hosts 指向空设备 (核心:不保存也不对比旧 Key)
# 这样每次连接都像是第一次,自然也就不会有“Key 变了”的报错
#UserKnownHostsFile: /dev/null
# 3. 配合这个参数可以保持安静,不再显示 "Warning: Permanently added..."
LogLevel: ERROR
# 4. (可选) 忽略主机 IP 检查
#CheckHostIP: "no"
##############以上可选开启############
#更安全的设置建议是:
# 生产机依然保持严格检查,防止中间人攻击
#"*.prod-safe":
##StrictHostKeyChecking: "yes"
#######
hosts:
# 定义一个基础模板
base:
User: root
Port: 7021
ForwardAgent: yes
ControlMS: auto
IdentityFile: ~/.ssh/id_ed25519
# 创建一个中间层模板
regional-base:
inherits: [base]
ForwardAgent: yes
# 按地区设置不同跳板
"us-*":
inherits: [regional-base]
Gateways: [sjc]
"eu-*":
inherits: [regional-base]
Gateways: [de]
"aws-*":
inherits: [awsJP]
Gateways: [aws1]
############可以多层继承#######
local:
User: al
Port: 22
IdentityFile: ~/.ssh/id_ed25519
ForwardAgent: yes
ControlMS: auto
awsJP:
User: admin
Port: 22
ForwardAgent: yes
ControlMS: auto
IdentityFile: ~/.ssh/configs/aws
###普通节点的配置如上面两个
###可以用*通配符开头
"*.eu":
User: root
IdentityFile: ~/.ssh/id_ed25519
# 这是区域的跳板机
#jump-server:
aws1:
HostName: 13.113.-.-
User: admin
Port: 22
IdentityFile: ~/.ssh/keys58/ec2.pem
sjc:
Hostname: 146.-.-
inherits: [base]
de:
Hostname: 178.-.-
inherits: [base]
#####################
t22:
inherits: [local]
Hostname: 192.168.-.-
us-bread:
HostName: 82.-.-
IdentityFile: ~/.ssh/keys58/bits
eu-dec:
HostName: 5.231.-.-
##us和eu开头的都走区域跳板##
# 正则表达式匹配示例
# "/^node-(\d+)$/":
# HostName: "192.168.1.%1" # %1 会被正则中的第一个捕获组替换
# 这个是给很多ip准备的;一般用不着高级 继承和嵌套规则 配合其他ssh参数
在 assh 的多重继承逻辑中,处理冲突遵循一个非常直观的原则:“由远及近,后者居上”。
当执行 inherits: [base, work] 时,字段的优先级顺序(从低到高)如下:
defaults块(全局最底层的默认值)base(列表中的第一个,优先级较低)work(列表中的最后一个,优先级高于前者)my-host自身(优先级最高,定义的字段会覆盖所有继承来的值)
🛠️ 举个具体的例子
假设你的配置文件如下:
hosts:
base:
User: guest
Port: 22
ForwardAgent: no
work:
User: admin
ForwardAgent: yes
# 注意:work 没有定义 Port
my-host:
inherits: [base, work]
Port: 2222 # 自己定义的 Port
最终编译(Build)出的结果:
当你连接 my-host 时,assh 合并后的逻辑是:
User: 变为admin。虽然base说是guest,但work在列表后面,覆盖了前者。Port: 变为2222。虽然base说是22,但my-host自身的定义拥有最高优先级。ForwardAgent: 变为yes。取自work。
🎨 继承合并逻辑示意图
- 从左到右合并:
assh先拿base的配置,然后把work的配置“拍”上去。如果有重名的 Key,work的值会顶替掉base的值。 - 自身覆盖: 最后把
my-host里的配置再“拍”上去,确保个性化设置生效。
💡 进阶技巧:多重继承的妙用
你可以利用这个特性构建非常复杂的网络逻辑,而不需要重复写代码。
场景:访问公司内网的数据库
hosts:
# 基础模板
base:
User: alvin
# 公司网络模板(需要走跳板机)
company-network:
Gateways: [jump-server]
ProxyJump: jump-server
# 数据库专用模板(需要开启隧道)
db-tunnel:
LocalForward: "5432 localhost:5432"
# 最终的主机:既属于公司网络,又是数据库,且我想换个用户名
prod-db:
inherits: [base, company-network, db-tunnel]
User: postgres # 会覆盖 base 里的User
HostName: 10.0.1.50
总结
- 列表顺序很重要:
[A, B]和[B, A]的结果可能完全不同(如果 A 和 B 有冲突字段)。 - 自身定义最稳: 任何你写在
my-host:下方的字段,绝对不会被继承过来的值覆盖。
如果你在合并后不确定最终生成的配置长什么样,可以运行:assh config list my-host
这会直接显示该主机合并后的所有最终参数,是排查冲突的利器。