Evy Meerman - Bongers

Passionate about a lot of things

XDG basedir: cleaning the clutter in my homedir

Published February 14, 2024

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"'
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 (' 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 or git config core.sshCommand
  • $ANSIBLE_SSH_COMMON_ARGS or ssh_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.


exe="$(type -ap ${0##*/} |
       grep -v "^${0}$" |
       head -1

exec ${exe} -F "${XDG_CONFIG_HOME}/ssh/config" "$@"

  1. Starting a file or directory name will cause the files to be hidden in directory listings. ↩︎

  2. The oldest record I have found is an email that was forwarded to the xdg mailing list. ↩︎

  3. That evolved from the information found on the Arch wiki, which refers to a blog post by Tom Vincent as a source. ↩︎

  4. That I know of ↩︎

  5. For example platforms offering hosted version control, like git or mercurial. ↩︎

  6. Which could be considered insecure or at least potentially exploitable. ↩︎