随着折腾的设备和云服务器越来越多,我们本地的 SSH Config 配置越来越复杂,为了解决这个问题,最近做了一些简单的尝试。
本文提到的开源软件 soulteary/ssh-config[1],为了让使用更放心,项目代码的单元测试覆盖 100%,确保数据转换是幂等、可靠的,能够放心的长时间使用。感兴趣可以自取,欢迎一键三连。
SSH Config Tool
熟悉我的朋友知道我换电子设备比较勤快[2],这几年敲字使用的 Mac 应该也已经更换了四五台左右。但是,不论我的“打字机”换成了哪一款,我都会在当前设备中用 Git 初始化一个 ~/.ssh
目录,导入包含了我的 RSA 密钥和绝大多数的服务器 SSH 连接配置。
这个目录的大致情况如下(主要配置包含config
文件和 config.d/*
文件目录中的文件):
# 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 多行的内容:
# cat ~/.ssh/config ~/.ssh/config.d/* | wc -l
528
而配置中的内容,其实非常的无聊,都是模版化的服务器连接参数堆叠:
# 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 行左右。
# cat test.yaml | wc -l
130
# cat test.yaml | ssh-config -to-ssh | wc -l
143
随便举个例子,如果我们想管理三台两两之间有相似配置的服务器,这个配置将类似这样,而非充斥大量冗余配置的 SSH Config,而这个配置可以 100% 稳定转换为幂等的 SSH Config:
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
接下来,我将分别聊聊这个工具如何使用,以及制作过程中的一些有趣的事情。
首先是软件的获取,如果你熟悉 Docker,那么我推荐你使用 Docker。如果你是极简主义者,也可以从 GitHub 发布页面[3]下载合适你的系统、CPU 架构的二进制文件。
软件的使用有两种模式,第一种是普通的命令行加参数:
ssh-config [options] <input_file> <output_file>
如果我们想要把 ~/.ssh/
目录中所有的配置都转换为 YAML 格式,并保存到当前目录的 test.yaml
文件中:
ssh-config -to-yaml -src ~/.ssh/ -dest test.yaml
如果你不想保存到某个文件中,而是想直接查看转换结果,只需要去掉 -dest
参数,转换文件的内容就直接展示出来啦:
# 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 管道,那么你可以使用下面的方式,来实现上面的命令相同的功能:
# 保存到某个文件
cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml > test.yaml
# 直接查看转换结果
cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml
上面的配置是不是看起来有一些冗余?小学的时候,我们就学过一个技巧,叫做“提取公因数”,这个技巧,我们可以用来改进这个配置文件。
上面的配置中,我们能够看到程序将每一个 Hosts 都转换为了一个单独的分组。每个分组中都有一些共同的配置,我们可以将它们挪到全局使用的配置 default
字段中。
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
后,执行转换命令:
cat test.yaml| ./ssh-config -to-ssh
就能够得到下面的配置转换结果啦:
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
你或许会想问,如果我想给每个分组都有自己的额外的“共同配置”,那么该如何处理呢?举个例子,我们在上面的配置中,再添加一台服务器:
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
字段中,将小组内的“公因数”提取出来:
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 配置看看?
# 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 pull soulteary/ssh-config:v1.1.0
# or
docker pull ghcr.io/soulteary/ssh-config:v1.1.0
和上文一样,使用的方式有直接操作文件,或者使用 Linux 管道。
如果你想直接操作文件,那么你需要将本地的文件映射到容器中,然后使用工具来处理文件即可:
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 交互式命令行环境,然后再使用工具操作配置文件:
docker run --rm -it -v `pwd`:/ssh soulteary/ssh-config:v1.1.0 bash
cat /ssh/test.yaml | ssh-config -to-yaml
在制作工具之前,第一件事是看能否不制作这个工具:或许开源社区有类似需求的人,实现了这样一个小工具。
虽然有一些工具,但是距离我的理想型,总归是差了一些。
在进行了一番查找,以及群友的推荐下,大概可以使用下面四个路径来完成 SSH 配置文件的管理:
我更换设备的频率保持在每年或最长每两年(有时候是半年),基于这个情况我越来越倾向使用最常见、最普适的高市场占比的软件,OpenSSH Client 是市占率非常高的家伙,除了路由器这类设备外,能够使用 SSH 进行交互的设备,几乎都在使用 OpenSSH 相关软件。所以,使用替代品,不一定是折腾后能够省心的事情。
因为我的服务器和设备数量并不少,基于可视化的界面方案在呈现效果和使用效率上来说,都会有一些体验上的损失。并且,除了 Web 技术栈之外的 “可视化” 方案在跨系统平台、CPU 架构、多设备的体验中需要付出额外的代价。即使我订阅的 Setapp 中有这个 SSH Config Editor 软件,考虑到上面的因素,我倾向选择不使用带 Web 界面之外的软件。
至于类似 Ansible 类似的持续集成常客的方案,管理配置不是不行,但是太过麻烦,上文中提到的功能,光看文档的时间就足够写完整个程序了。进行幂等的文件部署,是合适的,细粒度管理和更新配置,这类软件其实并不擅长。
让我开心的,但是又失望的软件,是一款 CLI 命令行软件。
开心的是,这款软件能够将 YAML 格式的 SSH Config 转换为 OpenSSH 能够使用的格式,定义了几乎就是我想要的清晰明了、很少冗余信息的配置。但,失望的是:
为了解决这几个原因,我决定参考这个软件配置的思路,重新实现一个能够支持 SSH Config 多种格式保存、转换功能,核心逻辑 100% 测试覆盖,不需要复杂外部依赖的小工具。
更加稳定、可靠、小巧、高效,确保使用这个工具处理数据的结果是准确的、体验是舒服的。
这个程序的本质是字符串处理工具,主要处理的字符串类型有下面三种:
因为工具设计里,这三种类型之间可以自由转换,所以,我们需要关注一种数据格式,或者抽象一种更简单的,可以横跨三个数据格式中的中间格式(类似“中间代码(IR)”)。
我定义的基础的中间代码类似下面这样,一个包含了所有 OpenSSH Client 支持的配置项的数据结构(源代码soulteary/ssh-config/internal/parser/define.go[10],120 行+):
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[11]):
...
// 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"`
}
在定义了“硬通货”和“最终数据格式”之后,我们就能够快速的展开工作了,只需要:
虽然每个功能都很简单,但是因为我们可以选择某个目录或者某个文件文件作为数据源,可以选择设置保存文件路径,也可以不设置路径,可以将数据转换为 YAML、转换为 JSON、转换为 SSH 配置等等,加上各种异常判断,各种组合下来,想要写一个能跑的程序简单,想要写一个 100% bugfree 的软件,其实还是要花一些时间的。
而这里,能够减少时间花费的秘诀,就是优先完成函数的设计和单元测试覆盖。 100% 的单元测试覆盖,可以让你在使用软件的时候拥有非常强的安全感,在重构或者为软件新增功能的时候,花最少的时间去完成功能的添加和验证,而不需要担心引入额外的风险。
最近的事情比较多,既然工具已经分享出来啦,那么这篇文章就先写到这里吧。
我们下篇文章再见。
--EOF
[1]
soulteary/ssh-config: https://github.com/soulteary/ssh-config
[2]
比较勤快: https://github.com/soulteary/Home-Network-Note
[3]
GitHub 发布页面: https://github.com/soulteary/ssh-config/releases
[4]
trzsz-ssh ( tssh ) : https://github.com/trzsz/trzsz-ssh
[5]
SSH Config Editor: https://www.hejki.org/ssheditor/
[6]
Brew: https://formulae.brew.sh/cask/ssh-config-editor
[7]
karlot/sshclick: https://github.com/karlot/sshclick
[8]
Ansible template: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html
[9]
bencromwell/sshush: https://github.com/bencromwell/sshush
[10]
soulteary/ssh-config/internal/parser/define.go: https://github.com/soulteary/ssh-config/blob/main/internal/parser/define.go
[11]
soulteary/ssh-config/internal/define/define.go: https://github.com/soulteary/ssh-config/blob/main/internal/define/define.go
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“喜欢” ,这些免费的鼓励将会影响后续有关内容的更新速度。