Templating SSH Client Configuration
Note: When I first got the idea for this I used gomplate, then I realized my dotfile manager of choice, chezmoi is better suited for my usecase. I have dumped the files I created in this gist just in case someone finds them useful.
In my my previous post, I explained how I use Match in my ssh_config to dynamically select my jump host. At first I had a few files in a git repository that I symlink to ~/.ssh/conf.d and have them includeed in ~/.ssh/config. But the more hosts I added the more unmanageable the soultion becoame. Recently I started using devpod for development and it started modifying ~/.ssh/config which started breaking in all sorts of ways, so I decided to redo my configuration and thought templating it would be a good idea. I ended up liking the end result and wanted to share.
~/.local/share/chezmoi/.chezmoitemplates/ssh_host:
1{{- $host := . -}}
2{{- range (default (list (list "always" "none")) (index . "proxy")) }}
3Match host {{ $host.name }},{{ $host.ips|join "," }}{{ if ne (index . 0) "none" }} exec "~/.ssh/network_detect/{{ index . 0 }}"{{ end }} {{- if not (contains "*" (index $host.ips 0)) }}
4HostName {{ index $host.ips 0 }}{{ end }}
5 User {{ index $host "user" | default "root" }}
6{{- if index $host "key" }}
7 IdentitiesOnly yes
8{{- if eq (printf "%T" $host.key) "string" }}
9 IdentityFile ~/.ssh/{{ $host.key }}
10{{- else -}}
11{{ range $host.key }}
12 IdentityFile ~/.ssh/{{ . }}
13{{- end -}}
14{{- end -}}
15{{- end -}}
16{{- range $env, $env_val := default nil (index $host "env") }}
17 SetEnv {{ $env }}={{ $env_val }}
18{{- end }}
19{{- range $option, $option_val := default nil (index $host "options") }}
20 {{ $option }} {{ $option_val }}
21{{- end }}
22 ProxyJump {{ index . 1 }}
23{{ end -}}
~/.local/share/chezmoi/private_dot_ssh/config.inc.tmpl:
1AddKeysToAgent yes
2{{- range values .ssh_servers -}}
3{{ range . }}
4{{ template "ssh_host" . }}
5{{- end -}}
6{{- end -}}
7{{- "" }}
8{{- $hosts := list }}
9{{- range (values .ssh_servers) }}
10{{- range . }}
11{{- $hosts = append $hosts .name }}
12{{- end }}
13{{- end }}
14# Allow auto completions for shells that support it
15{{- range ($hosts | uniq) }}
16Host {{ . -}}
17{{ end }}
18
19Host *
20 ForwardAgent no
21 ServerAliveInterval 30
22 ServerAliveCountMax 2
23 SetEnv TERM=xterm-256color
This allows storing server information as follows
~/.local/share/chezmoi/.chezmoidata/office.yml:
1ssh_servers:
2 office:
3 -
4 name: office-web1
5 ips:
6 - 10.1.0.1
7 key: ed25519
8 env:
9 TERM: xterm-ghostty
10 proxy:
11 -
12 - office
13 - none
14 -
15 - home
16 - home-bastion
17 -
18 - wireguard
19 - wg-bastion
~/.local/share/chezmoi/.chezmoidata/client1.yml:
1ssh_servers:
2 client1:
3 -
4 name: client1-infra
5 ips:
6 - 172.16.0.*
7 key:
8 - client1-web.pub
9 - client1-db.pub
10 proxy:
11 -
12 - office
13 - office-bastion
14 -
15 - wireguard
16 - wg-bastion
And you can even store per-machine configuration as follows
.config/chezmoi/chezmoi.json:
1{
2 "git": {
3 "autoCommit": true,
4 "autoPush": true
5 },
6 "data": {
7 "ssh_servers": {
8 "this_host_only":
9 [
10 {
11 "name": "host1",
12 "ips": [ "172.17.1.1" ]
13 }
14 ]
15 }
16 }
17}
The sub-keys under ssh_servers allows chezmoi to combine the data from mutiple files, making the configuration that little bit more maintainable.