动机和初心

最近用轻量的Ghostty作为ssh终端。抛弃了其他重型的。那么跳板机就成为了个问题。索性彻底折腾了下,不用再搞那个config配置。
assh也支持include分拆【语法不同】不过目前几十个小鸡用一个文件这样管理也可以了,简练了很多毕竟。

assh (Advanced SSH Config)

是一个基于 Go 编写的增强型 SSH 工具。它的核心逻辑是:用更现代、更强大的方式编写配置,然后将其动态生成或“翻译”给原生的 SSH 客户端。
https://github.com/moul/assh
对于管理几十个甚至上百个 VPS 节点的开发者来说,它是从“手动配置”进化到“配置即代码”的神器。


🚀 assh 的核心功能

  1. 配置继承 (Inheritance): 像面向对象编程一样,定义一个 base 模板,其他 Host 直接继承。
  2. 正则表达式支持: 可以用 Host /^prod-(.*)/ 这种语法批量匹配机器。
  3. 透明网关 (Gateways): 极大简化多级跳板机配置,支持链式跳转(A -> B -> C)。
  4. 动态主机: 可以通过钩子 (Hooks) 或脚本动态决定连接参数。
  5. 别名与重写: 支持比原生更灵活的别名系统。
  6. 本地/远程命令执行: 在连接前、连接后自动执行特定脚本。

实现的功能

根据区域跳板,命名如eu,us开头,直接ssh目标机就实现了优化机代理的目的。

assh可以和原来的.ssh/config配合使用,但每次修改都要build一下,我觉得比较麻烦。从实用的角度,我选择让assh全面接管。因为我通常只需要跳板机的功能。

#要在config里用Include config.assh
#本文舍弃这个方案
assh config build > ~/.ssh/config.assh

1. 安装

对于大部分系统,直接下载二进制文件或使用包管理器:

  • 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/assh

    2. 使用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] 时,字段的优先级顺序(从低到高)如下:

  1. defaults 块(全局最底层的默认值)
  2. base(列表中的第一个,优先级较低)
  3. work(列表中的最后一个,优先级高于前者)
  4. 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。

🎨 继承合并逻辑示意图

  1. 从左到右合并: assh 先拿 base 的配置,然后把 work 的配置“拍”上去。如果有重名的 Key,work 的值会顶替掉 base 的值。
  2. 自身覆盖: 最后把 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
这会直接显示该主机合并后的所有最终参数,是排查冲突的利器。