Historically, user specific configuration on unix and unix-like systems is stored in the user’s home directory as so-called dotfiles. Just like many other users do, I keep track of my dotfiles in a git repository. Tracking your dotfiles using a version control system such as git helps keep track of changes and makes is easy to keep configuration in sync across systems. While there are several approaches to managing dotfiles, this doesn’t change the fact that over time my home directory will be cluttered with (hidden1) files and directies.
For quite a while now, the XDG base directory specification has offered an alternative location for storing user configuration and other user specific application data. In addition to user directories, it states options to specify defaults for system wide application data. While I haven’t been able to find why the spec was initially created2, I can think of a few reasons for proposing such a standard:
- Separation of files based on purpose (cache vs config vs data vs state)
- It’s easier to clean up temporary files and caches
- It’s easier to back up (just) important files (like configuration)
- Security: managing a few well known paths (notably
$XDG_CONFIG_HOME
and$XDG_STATE_HOME
) vs managing a plethora of files and directies - A cleaner home directory
When I started moving configuration and data from my homedir to xdg base directories, I quickly found the Arch linux wiki, which has a page that catalogues software support. Most of the software that I track in my dotfiles repository already implements the specification. For most of the ones that don’t, the wiki documents managable workarounds. These workarounds mostly use environment variables already supported by the applications to get them to load their (primary) configuration file(s) in a different location. Those configuration files often provide means to specify locations for any other files or directories.
The challenge of getting SSH to comply
Unfortunately, not all software can be configured like that. Whereas for example
vim
, zsh
and gnupg
can be told to initialize differently ($VIMINIT
) or
to look in a different directory for initialization files ($ZDOTDIR
,
$GNUPGHOME
), other software doesn’t offer such means. For all the dotfiles I
track, that left two things to move: .profile
and .ssh
.
The first one I’m not even going to try to move, as it’s used by so many different applications that it’s simply impossible to account for all of them. The second one however is only used by one single piece of software I use. Challenge accepted.
First of all, the .ssh
directory serves two pieces of software that are
(usually) shipped togehter: an ssh client and an ssh server. I use OpenSSH and
while my approach, however involved it is, works for OpenSSH, it might not work
for other clients or servers.
Using my vim config3 as a starting point, I figured that as long as
I tell ssh
to look for my configuration somewhere else, I should be able to
get this done. Unlike vim
, ssh
doesn’t have any environment
variables4 to influence where to look for configuration or
initialization files. Luckily, it does have a commandline option to tell it to
use a different configuration file:
-F configfile
Specifies an alternative per-user configuration file. If a
configuration file is given on the command line, the sys‐
tem-wide configuration file (/etc/ssh/ssh_config) will be
ignored. The default for the per-user configuration file
is ~/.ssh/config. If set to “none”, no configuration files
will be read.
Because I want to save myself keystrokes when I reasonably can, I made an alias for this. In hindsight, it was a bad idea to rely on this, but more on that later. Until then:
mkdir -p 0700 "${XDG_CONFIG_HOME}/ssh"
mv ~/.ssh/config "${XDG_CONFIG_HOME}/ssh/config"
cat >> "${XDG_CONFIG_HOME}/zsh/aliases.zsh" <<'ALIASES'
alias scp='scp -F "${XDG_CONFIG_HOME}/ssh/config"'
alias sftp='sftp -F "${XDG_CONFIG_HOME}/ssh/config"'
alias ssh='ssh -F "${XDG_CONFIG_HOME}/ssh/config"'
alias sshfs='sshfs -F "${XDG_CONFIG_HOME}/ssh/config"'
ALIASES
source "${XDG_CONFIG_HOME}/zsh/aliases.zsh"
And one last thing, because of this: “If a configuration file is given on the
command line, the system-wide configuration file (/etc/ssh/ssh_config
) will be
ignored.”
echo "Include /etc/ssh/ssh_config" > "${XDG_CONFIG_HOME}/ssh/config"
Now I’m ready to move all other files to their rightful place, so what else do I find in this directory:
- public and private key files
- known_hosts file
- authorized_keys file
SSH keys
Public and private key files are very easy. While ssh
looks for some default
names, I already used non-default names to use different keys for different
purposes. Lucky for me, the ssh_config(5)
man page told me that the
IdentityFile
directive supports environment variables. All I need to do is to
update their paths:
Host github.com gitlab.com git.sr.ht
- IdentityFile .ssh/ed25519_git
+ IdentityFile ${XDG_DATA_HOME}/ssh/ed25519_git
Host personal-server
- IdentityFile .ssh/ed25519_personal
+ IdentityFile ${XDG_DATA_HOME}/ssh/ed25519_personal
Known hosts
The known_hosts
file can also easily be moved, because for this too, there’s a
configuration directive. According to the man page, this one too supports
environment variables. The simplest option would be to just move it without any
more consideration.
Host *
+ UserKnownHostsFile ${XDG_STATE_HOME}/ssh/known_hosts
Before starting with this path, I never even bothered to think about where known host keys were stored. When looking it up in the man page, I found that this directive also supports tokens.
From a security perspective, one should always verify server keys when prompted to by an ssh client:
The authenticity of host 'git.sr.ht (46.23.81.155)' can't be established.
ED25519 key fingerprint is SHA256:WXXNZu0YyoE3KBl5qh4GsnF1vR0NeEPYJAiPME+P09g.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Experience has taught me that no one ever bothers verifying them and instead
simply types yes to proceed. I myself am guilty of this too, which is exactly
why the token %h
caught my interest: %h The remote hostname.
Administrators of certain publicly accessable SSH servers5 publish the keys of their servers on their website, to allow users to verify them manually. Learning about this token quickly had me convinced that, using it, I should never again have to trust their keys on first use. So I looked up the keys for some of those services I use regularly, added them to my dotfiles repo and updated my SSH config like this:
Host *
+ UserKnownHostsFile ${XDG_STATE_HOME}/ssh/known_hosts ${XDG_STATE_HOME}/ssh/%h.keys
In hindsight $XDG_DATA_HOME
might have been a more appropriate place, but this
is the choice I made at the time. Since I originally wrote this config, I have
learned about the KnownHostsCommand
directive, which I now use, but that’s for
another post.
Authorized keys
That leaves the authorized_keys
file, which is only used by the ssh server.
This file I can only move if I have root access. This file however is one that I
don’t track in my dotfiles repository because it’s context dependent and often
controlled by config management systems. Another issue here is that the ssh
daemon doesn’t have access to the user environment when it’s validating
authorized keys, meaning that there is no way to read the value of xdg
variables. In the end I see three options:
- leave the file where it is
- hard code the path to the default value stated in the specification
- move the file out of the user’s home directory
For systems that aren’t my own, I simply won’t bother to move it and go with the first option, also because destribution of keys is usually out of my control. For my own systems I went with option two and kept the original location as fallback:
AuthorizedKeysFile .config/ssh/authorized_keys .ssh/authorized_keys
The alternative option might have been to move them to the system wide
$XDG_CONFIG_DIRS
and keep them in /etc/xdg/sshd/%u/authorized_keys
for
example, in which %u
is a token that represents the username.
AuthorizedKeysFile /etc/xdg/sshd/%u/authorized_keys .ssh/authorized_keys
Apart from the files that I found in my ~/.ssh
directory, there was one more
settings in my config that referred to that directory: ControlPath
. This
setting also supports environment variables, to this was also an easy fix:
Host *
ControlMaster auto
- ControlPath ~/.ssh/cm_sockets/%r@%h:%p.ctl
+ ControlPath ${XDG_RUNTIME_DIR}/%r@%h:%p.ctl
Closing notes
It’s over three years ago that I started down this path. I’ve learned that on some older systems I ran into, some of my configuration wasn’t compatible with older versions of OpenSSH (on Ubuntu 20.04 if I recall correctly).
Another issue I can into is that several programs use SSH under the hood and
have to be made ware of my ssh config having moved. This includes, among others
git
, ansible
and rsync
. I meantioned before that, in hindsight, it as a
bad idea to rely on aliases. This is the reason why. It certainly is possible to
have these programs look for my ssh config in a non-default location:
$GIT_SSH_COMMAND
orgit config core.sshCommand
$ANSIBLE_SSH_COMMON_ARGS
orssh_connection.ssh_common_args
configuration entry$RSYNC_RSH
or--rsh=...
The downside of doing this is pretty obvious: every single program that invokes
ssh
needs to be configured to invoke ssh
with the required command line
options. Until ssh
either implements some way to override the location of the
configuration file transparently6 or implements the xdg
base directory specificafion, the best option to override the default location
of the configuration file is probably to write a simple wrapper script and
place/symlink it in a directory that’s earlier in your PATH
than the location
of ssh
and related binaries.
#!/bin/bash
exe="$(type -ap ${0##*/} |
grep -v "^${0}$" |
head -1
)"
exec ${exe} -F "${XDG_CONFIG_HOME}/ssh/config" "$@"
-
Starting a file or directory name will cause the files to be hidden in directory listings. ↩︎
-
The oldest record I have found is an email that was forwarded to the xdg mailing list. ↩︎
-
That evolved from the information found on the Arch wiki, which refers to a blog post by Tom Vincent as a source. ↩︎
-
That I know of ↩︎
-
For example platforms offering hosted version control, like git or mercurial. ↩︎
-
Which could be considered insecure or at least potentially exploitable. ↩︎