本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2024年10月15日 统计字数: 10786字 阅读时间: 22分钟阅读 本文链接: https://soulteary.com/2024/10/15/manage-ssh-configuration-using-structure-data-ssh-config-tool.html ----- # 使用结构化数据管理 SSH 配置:SSH Config Tool 随着折腾的设备和云服务器越来越多,我们本地的 SSH Config 配置越来越复杂,为了解决这个问题,最近做了一些简单的尝试。 ## 写在前面 本文提到的开源软件是 [soulteary/ssh-config](https://github.com/soulteary/ssh-config),为了让使用更放心,项目代码的单元测试覆盖 100%,确保数据转换是幂等、可靠的,能够放心的长时间使用。感兴趣可以自取,欢迎一键三连。 ![SSH Config Tool](https://attachment.soulteary.com/2024/10/15/ssh-config-tool.jpg) 熟悉我的朋友知道我换电子设备[比较勤快](https://github.com/soulteary/Home-Network-Note),这几年敲字使用的 Mac 应该也已经更换了四五台左右。但是,不论我的“打字机”换成了哪一款,我都会在当前设备中用 Git 初始化一个 `~/.ssh` 目录,导入包含了我的 RSA 密钥和绝大多数的服务器 SSH 连接配置。 这个目录的大致情况如下(主要配置包含`config` 文件和 `config.d/*` 文件目录中的文件): ```bash # tree -L 4 ~/.ssh/ /Users/soulteary/.ssh/ ├── config ├── config.d │   ├── aliyun │   ├── discard │   ├── ... │   ├── homelab │   └── tencent ├── keys │   ├── github │   │   ├── id_rsa_github │   │   └── id_rsa_github.pub │   └── devices │   ├── devices-2018 │   ├── devices-2018.pub │   ├── devices-2020 │   ├── devices-2020.pub │   ├── devices-2022 │   ├── devices-2022.pub │   ├── devices-2024 │   └── devices-2024.pub ├── known_hosts ├── logo.png └── template.d └── keep-connect.conf ``` 随着时间推移,折腾的 HomeLab 设备、虚拟机和云服务器越来越多,这个 `.ssh` 目录中的配置文件也越来越冗余。 如果使用命令行来统计行数,配置内容有 500 多行的内容: ```bash # cat ~/.ssh/config ~/.ssh/config.d/* | wc -l 528 ``` 而配置中的内容,其实非常的无聊,都是模版化的服务器连接参数堆叠: ```bash # cat ~/.ssh/config Host * HostkeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa # 服务器 1 Host server1 Hostname 123.123.123.123 Port 1234 IdentityFile ~/.ssh/keys/your-key ControlPath ~/.ssh/server1-%r@%h:%p ControlPersist yes TCPKeepAlive yes Compression yes ForwardAgent yes # 服务器 2 Host server2 Hostname 123.234.234.234 Port 1234 IdentityFile ~/.ssh/keys/your-key ControlPath ~/.ssh/server2-%r@%h:%p ControlPersist yes TCPKeepAlive yes Compression yes ForwardAgent yes # 其他配置 Include config.d/homelab Include config.d/aliyun Include config.d/tencent ... ``` 为了解决这个问题,我写了一个简单的命令行工具 `ssh-config`,在将工具转换后的配置进行简单的调整后,**具备清晰明了结构**的 YAML 配置文件行数缩短到了之前的 25%(还有进一步优化空间)。从这个 YAML 配置文件转换出的 OpenSSH 使用的 SSH Config 行数,也只有 140 行左右。 ```bash # cat test.yaml | wc -l 130 # cat test.yaml | ssh-config -to-ssh | wc -l 143 ``` 随便举个例子,如果我们想管理三台两两之间有相似配置的服务器,这个配置将类似这样,而非充斥大量冗余配置的 SSH Config,而这个配置可以 100% 稳定转换为幂等的 SSH Config: ```yaml global: HostKeyAlgorithms: +ssh-rsa PubkeyAcceptedAlgorithms: +ssh-rsa default: Compression: "yes" ControlPersist: "yes" ForwardAgent: "yes" Port: "1234" TCPKeepAlive: "yes" Group server1: Common: ControlPath: ~/.ssh/server-1-%r@%h:%p Hosts: server1: Notes: your notes here config: HostName: 123.123.123.123 IdentityFile: ~/.ssh/keys/your-key1 server3: Notes: new server config: HostName: 123.124.123.124 IdentityFile: ~/.ssh/keys/your-key3 Group server2: Hosts: server2: config: ControlPath: ~/.ssh/server-2-%r@%h:%p HostName: 123.234.123.234 IdentityFile: ~/.ssh/keys/your-key2 User: ubuntu ``` 接下来,我将分别聊聊这个工具如何使用,以及制作过程中的一些有趣的事情。 ## SSH Config 配置管理工具的使用 首先是软件的获取,如果你熟悉 Docker,那么我推荐你使用 Docker。如果你是极简主义者,也可以从 [GitHub 发布页面](https://github.com/soulteary/ssh-config/releases)下载合适你的系统、CPU 架构的二进制文件。 软件的使用有两种模式,第一种是普通的命令行加参数: ```bash ssh-config [options] ``` 如果我们想要把 `~/.ssh/` 目录中所有的配置都转换为 YAML 格式,并保存到当前目录的 `test.yaml` 文件中: ```bash ssh-config -to-yaml -src ~/.ssh/ -dest test.yaml ``` 如果你不想保存到某个文件中,而是想直接查看转换结果,只需要去掉 `-dest` 参数,转换文件的内容就直接展示出来啦: ```yaml # ssh-config -to-yaml -src /.ssh/ global: HostKeyAlgorithms: +ssh-rsa PubkeyAcceptedAlgorithms: +ssh-rsa Group server1: Hosts: server1: Notes: your notes here config: Compression: "yes" ControlPath: ~/.ssh/server-1-%r@%h:%p ControlPersist: "yes" ForwardAgent: "yes" HostName: 123.123.123.123 IdentityFile: ~/.ssh/keys/your-key1 Port: "1234" TCPKeepAlive: "yes" Group server2: Hosts: server2: config: Compression: "yes" ControlPath: ~/.ssh/server-2-%r@%h:%p ControlPersist: "yes" ForwardAgent: "yes" HostName: 123.234.123.234 IdentityFile: ~/.ssh/keys/your-key2 Port: "1234" TCPKeepAlive: "yes" User: ubuntu ``` 当然,如果你喜欢 Linux 管道,那么你可以使用下面的方式,来实现上面的命令相同的功能: ```bash # 保存到某个文件 cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml > test.yaml # 直接查看转换结果 cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml ``` 上面的配置是不是看起来有一些冗余?小学的时候,我们就学过一个技巧,叫做“提取公因数”,这个技巧,我们可以用来改进这个配置文件。 ### 优化转换后的配置 上面的配置中,我们能够看到程序将每一个 Hosts 都转换为了一个单独的分组。每个分组中都有一些共同的配置,我们可以将它们挪到全局使用的配置 `default` 字段中。 ```yaml global: HostKeyAlgorithms: +ssh-rsa PubkeyAcceptedAlgorithms: +ssh-rsa default: Compression: "yes" ControlPersist: "yes" ForwardAgent: "yes" Port: "1234" TCPKeepAlive: "yes" Group server1: Hosts: server1: Notes: your notes here config: ControlPath: ~/.ssh/server-1-%r@%h:%p HostName: 123.123.123.123 IdentityFile: ~/.ssh/keys/your-key1 Group server2: Hosts: server2: config: ControlPath: ~/.ssh/server-2-%r@%h:%p HostName: 123.234.123.234 IdentityFile: ~/.ssh/keys/your-key2 User: ubuntu ``` 当我们将文件保存为 `test.yaml` 后,执行转换命令: ```bash cat test.yaml| ./ssh-config -to-ssh ``` 就能够得到下面的配置转换结果啦: ```bash Host * HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa # your notes here Host server1 Compression yes ControlPath ~/.ssh/server-1-%r@%h:%p ControlPersist yes ForwardAgent yes HostName 123.123.123.123 IdentityFile ~/.ssh/keys/your-key1 Port 1234 TCPKeepAlive yes Host server2 Compression yes ControlPath ~/.ssh/server-2-%r@%h:%p ControlPersist yes ForwardAgent yes HostName 123.234.123.234 IdentityFile ~/.ssh/keys/your-key2 Port 1234 TCPKeepAlive yes User ubuntu ``` 你或许会想问,如果我想给每个分组都有自己的额外的“共同配置”,那么该如何处理呢?举个例子,我们在上面的配置中,再添加一台服务器: ```yaml global: HostKeyAlgorithms: +ssh-rsa PubkeyAcceptedAlgorithms: +ssh-rsa default: Compression: "yes" ControlPersist: "yes" ForwardAgent: "yes" Port: "1234" TCPKeepAlive: "yes" Group server1: Hosts: server1: Notes: your notes here config: ControlPath: ~/.ssh/server-1-%r@%h:%p HostName: 123.123.123.123 IdentityFile: ~/.ssh/keys/your-key1 server3: Notes: new server config: ControlPath: ~/.ssh/server-1-%r@%h:%p HostName: 123.124.123.124 IdentityFile: ~/.ssh/keys/your-key3 Group server2: Hosts: server2: config: ControlPath: ~/.ssh/server-2-%r@%h:%p HostName: 123.234.123.234 IdentityFile: ~/.ssh/keys/your-key2 User: ubuntu ``` 上面的配置中,我们能够看到 “Group server 1” 有两台服务器连接配置,“server 1” 和 “server 3” 中有一些相似配置。这些配置和其他分组的服务器不一样,和上文一样,使用全局配置管理,有一些不合适。 我们可以在分组下的 `Common` 字段中,将小组内的“公因数”提取出来: ```yaml global: HostKeyAlgorithms: +ssh-rsa PubkeyAcceptedAlgorithms: +ssh-rsa default: Compression: "yes" ControlPersist: "yes" ForwardAgent: "yes" Port: "1234" TCPKeepAlive: "yes" Group server1: Common: ControlPath: ~/.ssh/server-1-%r@%h:%p Hosts: server1: Notes: your notes here config: HostName: 123.123.123.123 IdentityFile: ~/.ssh/keys/your-key1 server3: Notes: new server config: HostName: 123.124.123.124 IdentityFile: ~/.ssh/keys/your-key3 Group server2: Hosts: server2: config: ControlPath: ~/.ssh/server-2-%r@%h:%p HostName: 123.234.123.234 IdentityFile: ~/.ssh/keys/your-key2 User: ubuntu ``` 再次执行命令,转换为 SSH 配置看看? ```bash # cat test.yaml| ./ssh-config -to-ssh Host * HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa # new server Host server3 Compression yes ControlPath ~/.ssh/server-1-%r@%h:%p ControlPersist yes ForwardAgent yes HostName 123.124.123.124 IdentityFile ~/.ssh/keys/your-key3 Port 1234 TCPKeepAlive yes # your notes here Host server1 Compression yes ControlPath ~/.ssh/server-1-%r@%h:%p ControlPersist yes ForwardAgent yes HostName 123.123.123.123 IdentityFile ~/.ssh/keys/your-key1 Port 1234 TCPKeepAlive yes Host server2 Compression yes ControlPath ~/.ssh/server-2-%r@%h:%p ControlPersist yes ForwardAgent yes HostName 123.234.123.234 IdentityFile ~/.ssh/keys/your-key2 Port 1234 TCPKeepAlive yes User ubuntu ``` 符合预期的配置就出现啦,是不是还蛮简单的呢? 通过这个方式,我们就可以更清晰明了的管理我们的服务器连接配置啦。 ## Docker 使用 如果你使用 Docker ,可以使用下面的方式来下载软件的镜像: ```bash docker pull soulteary/ssh-config:v1.1.0 # or docker pull ghcr.io/soulteary/ssh-config:v1.1.0 ``` 和上文一样,使用的方式有直接操作文件,或者使用 Linux 管道。 如果你想直接操作文件,那么你需要将本地的文件映射到容器中,然后使用工具来处理文件即可: ```bash docker run --rm -it -v `pwd`:/ssh soulteary/ssh-config:v1.1.0 ssh-config -to-yaml -src /ssh/test.yaml -dest /ssh/abc.yaml ``` 如果你想使用管道来操作文件,我个人推荐先进入 Docker 交互式命令行环境,然后再使用工具操作配置文件: ```bash docker run --rm -it -v `pwd`:/ssh soulteary/ssh-config:v1.1.0 bash cat /ssh/test.yaml | ssh-config -to-yaml ``` ## 工具制作背后的事情 **在制作工具之前,第一件事是看能否不制作这个工具:或许开源社区有类似需求的人,实现了这样一个小工具。** 虽然有一些工具,但是距离我的理想型,总归是差了一些。 ### SSH 配置管理工具方案 在进行了一番查找,以及群友的推荐下,大概可以使用下面四个路径来完成 SSH 配置文件的管理: - OpenSSH Client 的替代品:[trzsz-ssh ( tssh ) ](https://github.com/trzsz/trzsz-ssh)。 - 基于编辑配置文件思路的一些软件,比如:[SSH Config Editor](https://www.hejki.org/ssheditor/)([Brew](https://formulae.brew.sh/cask/ssh-config-editor))、终端中做一些界面的思路:[karlot/sshclick](https://github.com/karlot/sshclick)、部署工具的模版思路:[Ansible template](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html)。 - 让我开心又有一些失望的软件:[bencromwell/sshush](https://github.com/bencromwell/sshush) ### 确定制作工具路径 我更换设备的频率保持在每年或最长每两年(有时候是半年),基于这个情况我越来越倾向使用最常见、最普适的高市场占比的软件,OpenSSH Client 是市占率非常高的家伙,除了路由器这类设备外,能够使用 SSH 进行交互的设备,几乎都在使用 OpenSSH 相关软件。**所以,使用替代品,不一定是折腾后能够省心的事情。** 因为我的服务器和设备数量并不少,基于可视化的界面方案在呈现效果和使用效率上来说,都会有一些体验上的损失。并且,除了 Web 技术栈之外的 “可视化” 方案在跨系统平台、CPU 架构、多设备的体验中需要付出额外的代价。即使我订阅的 Setapp 中有这个 SSH Config Editor 软件,**考虑到上面的因素,我倾向选择不使用带 Web 界面之外的软件。** 至于类似 Ansible 类似的持续集成常客的方案,管理配置不是不行,但是太过麻烦,上文中提到的功能,光看文档的时间就足够写完整个程序了。进行幂等的文件部署,是合适的,**细粒度管理和更新配置,这类软件其实并不擅长。** 让我开心的,但是又失望的软件,是一款 CLI 命令行软件。 开心的是,这款软件能够将 YAML 格式的 SSH Config 转换为 OpenSSH 能够使用的格式,定义了几乎就是我想要的清晰明了、很少冗余信息的配置。但,失望的是: 1. 软件只能够从 YAML 结构,转换出 SSH Config,是单向处理。 2. 软件的核心转换过程,没有任何单元测试覆盖保障正确性。 3. 软件明明只有一个单一转换的功能,但是引用了一堆外部依赖。 4. 软件或许是受到了 1.x 版本的 Python 程序的编写影响,实现的有一些不好修改,上面三个问题想要提 PR 改进,稍微有点难度。 为了解决这几个原因,我决定参考这个软件配置的思路,重新实现一个能够支持 SSH Config 多种格式保存、转换功能,核心逻辑 100% 测试覆盖,不需要复杂外部依赖的小工具。 **更加稳定、可靠、小巧、高效,确保使用这个工具处理数据的结果是准确的、体验是舒服的。** ### 实践经验分享 这个程序的本质是字符串处理工具,主要处理的字符串类型有下面三种: - OpenSSH Client 使用的 SSH Config - 通用的 YAML 格式 - 通用的 JSON 格式 因为工具设计里,这三种类型之间可以自由转换,所以,我们需要关注一种数据格式,或者抽象一种更简单的,可以横跨三个数据格式中的中间格式(类似“中间代码(IR)”)。 我定义的基础的中间代码类似下面这样,一个包含了所有 OpenSSH Client 支持的配置项的数据结构(源代码[soulteary/ssh-config/internal/parser/define.go](https://github.com/soulteary/ssh-config/blob/main/internal/parser/define.go),120 行+): ```go type HostConfig struct { Host string `yaml:"Host,omitempty"` Match string `yaml:"Match,omitempty"` ... Ciphers string `yaml:"Ciphers,omitempty"` ClearAllForwardings string `yaml:"ClearAllForwardings,omitempty"` ... HostName string `yaml:"HostName,omitempty"` ... Port string `yaml:"Port,omitempty"` ... } ``` 上面这个数据结构就是三种配置“数据流通”过程中的硬通货。由于三种格式的使用场景和客观要求原因,三种格式的最终数据结构是有一些差异的,所以我们还需要为三种数据格式定义新的数据结构(源代码 [soulteary/ssh-config/internal/define/define.go](https://github.com/soulteary/ssh-config/blob/main/internal/define/define.go)): ```go ... // ssh config type HostConfig struct { Name string `yaml:"Name,omitempty"` Notes string `yaml:"Notes,omitempty"` Config map[string]string Extra HostExtraConfig `yaml:"Extra,omitempty"` } // json type HostConfigDataForJSON map[string]string type HostConfigForJSON struct { Name string `json:"Name,omitempty"` Notes string `json:"Notes,omitempty"` Data HostConfigDataForJSON `json:"Data,omitempty"` } // yaml type GroupConfig struct { Prefix string `yaml:"Prefix,omitempty"` Common map[string]string `yaml:"Common,omitempty"` Hosts map[string]HostConfig `yaml:"Hosts,omitempty"` } type YAMLOutput struct { Global map[string]string `yaml:"global,omitempty"` Default map[string]string `yaml:"default,omitempty"` Groups map[string]GroupConfig `yaml:",inline"` } ``` 在定义了“硬通货”和“最终数据格式”之后,我们就能够快速的展开工作了,只需要: - 完成不同格式到中间数据格式的相互转换,工具就能够支持多种文件格式的转换了。 - 完成基础的 IO 操作,工具就能够实现跟进需求读取配置文件,转换并保存新的配置文件的功能了。 - 完成标准管道操作,工具就能够支持 Linux 标准的 Pipeline 管道操作了。 虽然每个功能都很简单,但是因为我们可以选择某个目录或者某个文件文件作为数据源,可以选择设置保存文件路径,也可以不设置路径,可以将数据转换为 YAML、转换为 JSON、转换为 SSH 配置等等,加上各种异常判断,各种组合下来,想要写一个能跑的程序简单,想要写一个 100% bugfree 的软件,其实还是要花一些时间的。 而这里,**能够减少时间花费的秘诀,就是优先完成函数的设计和单元测试覆盖。** 100% 的单元测试覆盖,可以让你在使用软件的时候拥有非常强的安全感,在重构或者为软件新增功能的时候,花最少的时间去完成功能的添加和验证,而不需要担心引入额外的风险。 ## 最后 最近的事情比较多,既然工具已经分享出来啦,那么这篇文章就先写到这里吧。 我们下篇文章再见。 --EOF