Shami's Blog

DevOps because uptime is not optional

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.

About Me

Dev gone Ops gone DevOps. Any views expressed on this blog are mine alone and do not necessarily reflect the views of my employer.

Categories