Shami's Blog

Sysadmin, Because Even Developers Need Heroes

Templating SSH Client Configuration

2024-08-17

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:

{{- $host := . -}}
{{- range (default (list (list "always" "none")) (index . "proxy")) }}
Match 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)) }}
HostName {{ index $host.ips 0 }}{{ end }}
  User {{ index $host "user" | default "root" }}
{{- if index $host "key" }}
  IdentitiesOnly yes
{{- if eq (printf "%T" $host.key) "string" }}
  IdentityFile ~/.ssh/{{ $host.key }}
{{- else -}}
{{ range $host.key }}
  IdentityFile ~/.ssh/{{ . }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- range $env, $env_val := default nil (index $host "env") }}
  SetEnv {{ $env }}={{ $env_val }}
{{- end }}
{{- range $option, $option_val := default nil (index $host "options") }}
  {{ $option }} {{ $option_val }}
{{- end }}
  ProxyJump {{ index . 1 }}
{{ end -}}

~/.local/share/chezmoi/private_dot_ssh/config.inc.tmpl:

AddKeysToAgent yes
{{- range values .ssh_servers -}}
{{ range . }}
{{ template "ssh_host" . }}
{{- end -}}
{{- end -}}
{{- "" }}
{{- $hosts := list }}
{{- range (values .ssh_servers) }}
{{- range . }}
{{- $hosts = append $hosts .name }}
{{- end }}
{{- end }}
# Allow auto completions for shells that support it
{{- range ($hosts | uniq) }}
Host {{ . -}}
{{ end }}

Host *
  ForwardAgent no
  ServerAliveInterval 30
  ServerAliveCountMax 2
  SetEnv TERM=xterm-256color

This allows storing server information as follows

~/.local/share/chezmoi/.chezmoidata/office.yml:

ssh_servers:
  office:
    -
      name: office-web1
      ips:
        - 10.1.0.1
      key: ed25519
      env:
        TERM: xterm-ghostty
      proxy:
        -
          - office
          - none
        -
          - home
          - home-bastion
        -
          - wireguard
          - wg-bastion

~/.local/share/chezmoi/.chezmoidata/client1.yml:

ssh_servers:
  client1:
    -
      name: client1-infra
      ips:
        - 172.16.0.*
      key:
        - client1-web.pub
        - client1-db.pub
      proxy:
        -
          - office
          - office-bastion
        -
          - wireguard
          - wg-bastion

And you can even store per-machine configuration as follows .config/chezmoi/chezmoi.json:

{
  "git": {
    "autoCommit": true,
    "autoPush": true
  },
  "data": {
    "ssh_servers": {
      "this_host_only":
        [
          {
            "name": "host1",
            "ips": [ "172.17.1.1" ]
          }
        ]
    }
  }
}

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.

twitter linkedin