Per-Client Git Configurations for Consultants

Per-Client Git Configurations for Consultants

Throughtout my career, I’ve often stumbled across the following problem:

  • I use Github for my private projects, using my personal email address, signing commits with my private SSH key.
  • My company uses Github for my company projects, where I author commits with my company email address and sign them with my company SSH key.
  • My clients use Github and Gitlab for client projects, where I author commits with my client email address and sign them with my client SSH key.
  • My private SSH keys are not authorized for the client Github repositories.
  • All my SSH keys are on my Yubikey, and I don’t want to switch keys all the time.

Default configuration

Most folks only ever use a single top-level ~/.gitconfig. However, git allows you to define multiple configuration files and load them conditionally based on the path of the repository you are working in.

To get started, I created a default configuration file in ~/.gitconfig:

[user]
    name = Felix Hammerl
    email = <company email>
    signingkey = ~/.ssh/id_ed25519_sk_work

[gpg]
    format = ssh

[commit]
    gpgsign = true

[url "git@github.com:"]
    insteadOf = https://github.com/

[color]
    ui = true
    branch = auto
    diff = auto
    interactive = auto
    status = auto

[core]
    editor = vim

[push]
    default = simple
      autoSetupRemote = true

[pull]
    rebase = true

[help]
    autocorrect = 0

[init]
    defaultBranch = main

Given that this is my company’s machine, by default I am using my company identity.

My Machine’s Folder Structure

In order to use different identities and keys, you need to:

  • Split your projects into directories that match the client or project you are working on.
  • Initialize empty git repositories in each of these directories. These are never actually used, but they allow you to load the correct configuration file.
  • Create additional git configuration files for each client or project you are working on.
~/Projects/
    private/
        .git/ <- initialized, but unused
        .gitconfig.private <- personal configuration
        project-a/
            .git/
        project-b/
            .git/
    <company>/
        .git/ <- initialized, but unused
        .gitconfig.<company> <- company-specific configuration
        project-c/
            .git/
        project-d/
            .git/
    <client>/
        .git/ <- initialized, but unused
        .gitconfig.<client> <- client-specific configuration
        project-e/
            .git/
        project-f/
            .git/

Please note that the angled brackets <...> are placeholders for the actual names, which I will not disclose here for obvious reasons.

Handling multiple .gitconfig Files

The private directory is for my personal projects, the company directory is for my company projects, and the client directory is for my client projects. Notice that each category has its own .gitconfig file.

Here is a configuration for my personal projects, at ~/.gitconfig.private:

[user]
    name = Felix Hammerl
    email = felix.hammerl@gmail.com
    signingkey = ~/.ssh/id_ed25519_sk_private

[url "git@github-private:"]
    insteadOf = git@github.com:

This I have a configuration for a client where I sign my commits with , at ~/.gitconfig.<client>:

[user]
    name = <client identity>
    email = <client email>
    signingkey = ~/.ssh/id_ed25519_sk_<company>

[url "git@github-<client>:<client-gh-org>/"]
    insteadOf = git@github.com:<client-gh-org>/

[url "git@github-<client>:<client-gh-org>/"]
    insteadOf = https://github.com/<client-gh-org>/

What I am doing here is instruct git which SSH key to use for signing and I am rewriting the host names so that I can use different SSH keys for the same Github host.

So now let’s conditionally load these configurations:

  • Add the includeIf lines to your ~/.gitconfig file, matching the paths of the directories containing the empty repositories.
  • Specify the path to the corresponding configuration file for each directory.
  • Within the client directories we created per client, clone the actual repositories you are working on as you normally would.

Here is how this looks at the end of my ~/.gitconfig file:

[includeIf "gitdir:~/Projects/private/"]
    path = ~/Projects/private/.gitconfig.private

[includeIf "gitdir:~/Projects/<client>/"]
    path = ~/Projects/<client>/.gitconfig.<client>

Also, please do not forget the trailing slash in the path!

SSH Configuration

Here is where it gets interesting. How do you differentiate which SSH key to use for the same host (Github), where some keys have access to the client Github Enterprise, while others do not?

Host github-private
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_sk_private
    IdentitiesOnly yes
    IdentityAgent none

Host github-<client>
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_sk_<client>
    IdentitiesOnly yes
    IdentityAgent none

Host *
    IdentityFile ~/.ssh/id_ed25519_sk_<company>
    IdentitiesOnly yes
    IdentityAgent none

When I git clone a repositoy from the client, git clone/pull/push/... git rewrites replaces the hostname github.com with github-<client> as instructed in the client-specific .gitconfig file, and uses the corresponding SSH key.

In case you wonder why I am using IdentitiesOnly yes and IdentityAgent none: I am using a Yubikey to store my SSH keys, so if you don’t set IdentityAgent none, you will not get the prompt Confirm user presence for key when pushing, which can be annoying. I am not sure where this bug originates from, but it is a known issue. Also, you will not ever need the SSH Agent when using Github.

Multiple SSH Keys on a Yubikey

Now some of you might ask: “But my Yubikey can only store a single SSH resident key, how do you manage to use multiple keys?”

The solution is to use different scopes for the keys, like so:

  • To create my personal key: ssh-keygen -t ed25519-sk -O resident -O application=ssh:private -C "felix.hammerl@gmail.com" -f ~/.ssh/id_ed25519_sk_private
  • To create my company key: ssh-keygen -t ed25519-sk -O resident -O application=ssh:<company> -C "<company email>" -f ~/.ssh/id_ed25519_sk_<company>
  • To create my client key: ssh-keygen -t ed25519-sk -O resident -O application=ssh:<client> -C "<client email>" -f ~/.ssh/id_ed25519_sk_<client>

This way, the Yubikey will hold the SSH keys without conflict and SSH discovers them correctly.

Conclusion

Et voilà! You can now work on different projects with different identities, without having to worry about accidentally using the wrong identity or the wrong key to push a commit.