Configuration management with git on Debian

Ah, configuration files. The place there’s a guaranteed opportunity of edit conflict (between the package author, and you), and no really satisfying solution.

My understanding is that, on RPM-based distributions, if a configuration file is modified by the user, it stops being updated. Permanently. On Debian and derivatives, it asks you a question with no good answer: “Do you want to lose your configuration changes, or get a non-working software?”.

With most packages, it’s annoying, but not too bad; you can often put your configuration changes in an override file, that will be untouched by package upgrades and it should keep working. But I happen to use Dovecot, an IMAP server which has a complicated configuration (in many files), which cannot really be configured in overrides, and which changes pretty often.

I decided to tackle the problem once and for all.


The trick

I want to put the Dovecot configuration in a Git repository, with two branches: one for the default distribution configuration, and one for my customized configuration. When the Dovecot package is upgraded, it will put the new configuration files on the “distribution” branch. I can them commit the changes, then merge (or rebase) them on my customized branches. That way, I have more control over the process.

The problem is that I need to convince the installer to write the files on a Git branch. Fortunately, Git has a worktree feature that multiple branches to be checked out simultaneously in different directories. I can have my customized branch in /etc/dovecot and my distribution branch in /etc/dovecot.dist. That way, Dovecot will read the customized configuration fine, but I still need to tell the installer to write the new files in /etc/dovecot.dist and not /etc/dovecot.

Fortunately, Debian has a tool to do just that: dpkg-divert. It’s basically a way to tell the package system, “If a package want to install a file at /some/location/x, install it at /some/location/y instead”. This is called a diversion, and is mainly used by packages to avoid file conflicts with other packages. You can run dpkg-divert --list to see the list of active diversions on your system.

A small wrinkle is that Dovecot does not use the package system to install its configuration files; it uses a tool called ucf (“Update Configuration Files”), which makes configuration management a bit easier by allowing three-way merges during updates. This means that the files in /etc/ are created by ucf, not dpkg. Fortunately, ucf understands and respects diversions, so the procedure should still work.

So now we’re all set. Here is what I did:

Clean state configuration

Since my Dovecot configuration is pretty old (with files last modified in 2011!), I wanted to start clean with a new configuration. I looked at Dovecot post-install script and saw that it stores its default configuration in /usr/share/dovecot/ so I copied the files from there:

# mv /etc/dovecot /etc/dovecot.bak
# mkdir /etc/dovecot
# cp -r /usr/share/dovecot/conf.d /usr/share/dovecot/dovecot.conf /usr/share/dovecot/dovecot-dict-auth.conf.ext /usr/share/dovecot/dovecot-dict-sql.conf.ext /usr/share/dovecot/dovecot-sql.conf.ext /etc/dovecot/
# mkdir -m700 /etc/dovecot/private/
# ln -s /var/lib/dehydrated/certs/xxx/privkey.pem /etc/dovecot/private/dovecot.key 
# ln -s /var/lib/dehydrated/certs/xxx/fullchain.pem /etc/dovecot/private/dovecot.pem

The default Dovecot configuration loads its certificate and key from /etc/dovecot/private/dovecot.{pem,key}, which are symlinks to a fake certificate. Instead of changing the configuration, I decided to just make the symlinks point to my real certificate (a Let’s Encrypt certificate managed by dehydrated). The post-install script doesn’t touch those symlinks once they are created, so that should be fine.

Creating the Git repository

Since I’m editing Dovecot’s configuration as root, I needed to configure Git as root by running git config --global --edit and set a username/email.

# cd /etc/dovecot
# git init -b main .
Initialized empty Git repository in /etc/dovecot/.git/
# echo '/private/' > .gitignore
# git add .
# git commit -m 'Initial configuration from distro'
[main (root-commit) 56c5310] Initial configuration from distro
 30 files changed, 2218 insertions(+)
…
# git branch distro
# git worktree add ../dovecot.dist distro
Preparing worktree (checking out 'distro')
HEAD is now at 56c5310 Initial configuration from distro
# git branch
+ distro
* main
# cd /etc/dovecot.dist
# git branch
* distro
+ main

So we have the main branch on /etc/dovecot/ and the distro branch on /etc/dovecot.dist, as expected.

Configuration

I can then modify the configuration in /etc/dovecot/ and verify that it works. Once it’s done, I add a diversion for all files that I modified. For instance, I modified the file conf.d/10-auth.conf, so I run:

# dpkg-divert --local --no-rename --divert /etc/dovecot.dist/conf.d/10-auth.conf --add /etc/dovecot/conf.d/10-auth.conf
Adding 'local diversion of /etc/dovecot/conf.d/10-auth.conf to /etc/dovecot.dist/conf.d/10-auth.conf'

I do the same for all modified files, then I commit them into Git.

Upgrade

Now, package upgrades should go smoothly, since the package manager/ucf only sees unmodified files. Since not all files were diverted (maybe I should do it…), it’s possible that some files were modified/added on the customized branch. I move all these changes to the distro branch and cancel them on the main branch.
Then, I commit all changes on the distro branch, then merge/rebase it into the customized branch. Done!

Caveats

One issue to be aware of: file permissions. Don’t forget that the .git directory contains the full history of all your configuration files, and is by default world-readable. Even if you restrict access to one configuration file (because it contains database passwords for instance), people can still read the file contents by fetching it from the Git commit data (by running git show HEAD:thefile, for instance) 1. To avoid issues, you should make sure that the .git repository is not world-readable.


  1. This issue with accessible .git folders is not limited to local access — If you deploy a website that is hosted on a Git repository, make sure that the .git folder is not accessible from the web! If it is, people may be able to extract the raw files from your webserver, including any secrets that were committed to the repository.