Managing dotfiles with Git and encryption

A simple minimum-dependency approach

For any developer using Unix-like systems, their personal collection of dotfiles is likely to become important as they get customized more and more. Hiding files starting with . on Unix was not originally intended but using such files to store the configuration of various programs has long since been standard.

Some time ago, I found myself looking for a way to conveniently manage my dotfiles across several different computers. Putting them in a Git repository seemed the natural choice. Eventually, other requirements for easier management of the files came up, so I am sharing some ideas from the approach I use.

Requirements

First there is the matter of my personal requirements for dotfile management. Yours may well be different, but here is what I need:

  • No extra dependencies. I need a solution that works without Python, Ruby or anything else likely to require installation.
  • Solution that works on Linux systems and on Cygwin.
  • The ability to selectively encrypt some Bash aliases. Can also apply to other files.
  • One-line installation. After having cloned the Git repository on a new system, I want my familiar settings to appear after running one command.

Overall Setup

The first step is of course to put the dotfiles in a Git repository. I keep a folder ~/dotfiles on my machine that is the repository. So what else is there to do?

First, I quickly found that copying dotfiles from the repository to their actual locations is inconvenient, even with a script. Therefore I use symlinks. My ~/.bash_aliases is actually a symlink to ~/dotfiles/bash/.bash_aliases, and so on. The symlinking ensures that any change I make locally is also a change in the Git working tree, which I can then easily commit and push.

The next finding was that it’s inconvenient to have the structure of my repository follow the structure of my $HOME entirely. For instance, it’s quite convenient to create a bash subfolder in the repository that has all of my Bash configuration files. This clearly means that I need to use some kind of mapping for my setup script, specifying which files should be symlinked where.

Finally, it became clear that the setup script should be possible to run repeatedly without changing its behaviour (or to use a fancier expression, the script should be idempotent). It should nonetheless handle a situation where some of the files already exist on the system.

Encrypting sensitive aliases

I believe in sharing for the purposes of education and inspiration. My dotfiles are publicly hosted on GitHub. However, it is necessary to avoid publishing some of my Bash aliases - I do not want them on GitHub, and I do not necessarily want them on every system I set up. The reasons are quite mundane, for instance, some aliases that I use at work include company URLs that should ideally be kept private.

I accomplish that by having a separate folder in the repository bash/.bash_aliases.d that contains encrypted files. The setup script will then prompt for a password to decrypt these files. This solution is convenient and only depends on the presence of openssl on the system.

Why did I choose OpenSSL encryption? Normally gpg would be my first choice, but there’s no need for asymmetric encryption in my use case, and openssl is installed by default on more systems than gpg, as far as I’m aware.

And in order to actually use the decrypted aliases, if any, here is the part of my .bashrc that activates them:

     if [ -d ~/.bash_aliases.d ]; then
           shopt -s nullglob
           shopt -s dotglob
           for f in ~/.bash_aliases.d/* ;
               do
               ext=${f##*.}
               if ! [ "$ext" == "des3" ] && [ "$f" == ".gitignore" ]; then
                   . "$f"
               fi
           done
           shopt -u nullglob
           shopt -u dotglob
       fi

Setup script

Now that I’ve described the overall approach I have taken, it’s time to show what my setup script currently looks like. The setup actually consists of two files, the setup script itself and the configuration file containing the mapping of symlinks. Here is the config file:

# Vim
.vimrc:.vimrc
.vim/plugin:vim-plugins

# Bash, aliases
.bashrc:bash/.bashrc
.bash_aliases:bash/.bash_aliases
.bash_aliases.d:bash/.bash_aliases.d

# Git
.gitconfig:.gitconfig

# KDE apps
.kde/share/apps/konsole:kde/share/apps/konsole
.kde/share/config/yakuakerc:kde/share/config/yakuakerc
.kde/share/config/konsolerc:kde/share/config/konsolerc

The configuration is simply a list of colon-separated paths, where the first part indicates the path under $HOME and the second the path relative to my dotfiles repository.

And the setup.sh:

#!/bin/bash

function link()
{
	if ! [ -e "$1" ]; then
		echo "$1 does not exist, skipping"
	fi
	ln -s ${1} ${2}
}

mapfile -t CONFARR < <(cat conf | grep -v "^#")

for entry in ${CONFARR[@]}; do
	src=${entry##*:}
	dst=~/${entry%%:${src}}
	src=`pwd`/${src}
	if [ -e "$dst" ]; then
		target=`readlink "$dst"`
		if [ "$target" == "$src" ]; then
			echo "Link $dst up-to-date, skipping..."
			continue
		else
			# File exists, is not a link to my dotfiles
			echo "$dst exists and does not point to your dotfiles"
			read -p "Overwrite? (y/n) " -n 1 -r
			echo ""
			if [[ ! "$REPLY" =~ [Yy]$ ]]; then
				continue
			else
				mv "$dst" "$dst".bak
				echo "Backing up to $dst.bak"
			fi
		fi
	fi
	link "$src" "$dst"
done


if hash openssl 2>/dev/null; then
	#Decrypt additional files
	if [ -d `pwd`/bash/.bash_aliases.d ]; then
		for encfile in ./bash/.bash_aliases.d/*.des3 ; do
			fname=$(basename "$encfile")
			decfile="./bash/.bash_aliases.d/${fname%.*}"
			openssl des3 -d -salt -in "$encfile" -out "$decfile"
			# If it failed due to wrong decryption key...
			# the decrypted file is just crap. And file will
			# not even exist if openssl got a sigint
			if [ "$?" -eq 1 ] && [ -e "$decfile" ]; then
				rm "$decfile"
			fi
		done
	fi
else
	echo "OpenSSL not found, additional aliases will not be decrypted"
fi

I am using the Bash mapfile builtin to read my configuration, and then create symlinks according to that configuration. As a precaution, I ask about overwriting any pre-existing files, and back them up in case they do get overwritten.

The last part of the script checks for openssl on the system and, if it is present, tries to decrypt any files with the des3 extension in the folder I’ve allocated for encrypted files. To prevent the accidental push of non-encrypted files from that folder, I have .gitignore set up to skip everything that doesn’t have the des3 extension. bash/.bash_aliases.d/.gitignore:

*
!*.des3
!.gitignore

What Next?

In this post, I am not going to describe any specific settings I have in my dotfiles. Instead, it’s meant as a starting point if you’re wondering how to set up your own Git repository for dotfiles and encrypt some of them. If you are looking for inspiration for your own dotfiles, I suggest searching online for vimrc or any other specific file you’re interested in. Or you may just for someone’s dotfiles repository, including my own, although I believe it’s best to start small to find something that truly works well for you.

 
comments powered by Disqus